diff --git a/docs/arrow-functions.md b/docs/arrow-functions.md new file mode 100644 index 000000000..b665cf23c --- /dev/null +++ b/docs/arrow-functions.md @@ -0,0 +1,124 @@ +# Arrow Functions + +Arrow functions are a short way to write a function whose body is a single expression, modeled on the `=>` syntax from TypeScript/JavaScript. They are most useful for small callbacks passed to other functions. + +```brighterscript +adults = utils.filter(people, (p) => p.age >= 18) +``` + +transpiles to + +```brightscript +adults = utils_filter(people, function(p) + return p.age >= 18 +end function) +``` + +## Syntax + +An arrow function is a parenthesized parameter list, an optional return type, `=>`, and a single expression that becomes the function's return value. Parameters use the same syntax as regular functions, so types, optional parameters and default values all work. + +```brighterscript +' no params +getZero = () => 0 + +' single param (parens are required, even for one param) +double = (x) => x * 2 + +' multiple params +add = (a, b) => a + b + +' typed params and return type +add = (a as integer, b as integer) as integer => a + b + +' optional params with default values +greet = (name = "world") => "hello " + name + +' passed as an argument +names = map(people, (p) => p.name) + +' returning an associative array literal (wrap it in parens) +makePoint = (x, y) => ({ x: x, y: y }) +``` + +The body extends as far to the right as possible, so `(a) => a ? 1 : 2` returns the whole ternary expression. + +## Transpilation + +An arrow function compiles to an ordinary anonymous function that returns its expression: + +```brighterscript +add = (a as integer, b as integer) as integer => a + b +``` + +```brightscript +add = function(a as integer, b as integer) as integer + return a + b +end function +``` + +When the body is a call to something that returns nothing (a `sub`, or a built-in method like `push`), the arrow function compiles to a `sub` whose body is a plain statement, since `return list.push(item)` is not valid on device: + +```brighterscript +addItem = (list as integer[], item as integer) => list.push(item) +``` + +```brightscript +addItem = sub(list as dynamic, item as integer) + list.push(item) +end sub +``` + +## Type checking + +Arrow functions are type-checked exactly like anonymous functions. They can be used anywhere a [function type](function-types.md) is expected, and their arguments and return values are validated. + +Without an explicit return type, the return type is inferred from the body expression: + +```brighterscript +sub apply(callback as function(x as integer) as integer) +end sub + +' ok +apply((x as integer) => x * 2) + +' error: "function (x as string) as string" is not compatible with "function (x as integer) as integer" +apply((x as string) => x + "!") +``` + +## No closures (yet) + +Arrow functions do **not** capture variables from the function they are written in. They follow the same scoping rules as regular anonymous functions: they can see parameters, file-level functions, namespaces, classes and globals, but not the locals of the enclosing function. Referencing one is a compile error: + +```brighterscript +sub main() + factor = 2 + ' error: Arrow functions cannot use variable 'factor' from the enclosing function + scale = (x) => x * factor + + ' pass it in as a parameter instead + scale = (x, by) => x * by + print scale(10, factor) +end sub +``` + +`m` is also not allowed inside an arrow function. Pass the value you need in as an argument instead: + +```brighterscript +' error: 'm' cannot be used inside an arrow function +render = (data) => m.view.setData(data) + +' ok +render = (data, view) => view.setData(data) +render(data, m.view) +``` + +Both restrictions are deliberate. Closures are being designed separately (see [#530](https://github.com/rokucommunity/brighterscript/issues/530)) and will most likely reuse the `=>` syntax, so what "capturing a variable" and `m` mean inside an arrow function is left open until then. Code that works today will keep working unchanged once closures exist. + +An arrow function that captures nothing and does not use `m` is a plain BrightScript function. It can be passed anywhere a function can: parameters typed `as function` or with a function type, untyped (`dynamic`) parameters, and compiled `.brs` libraries that simply call `callback(x)`. + +## Limitations + +- The body must be a single expression. For multiple statements, use a regular `function ... end function`. +- Parameters must be in parentheses, even when there is only one: `(x) => x * 2`, not `x => x * 2`. +- Arrow functions are a BrighterScript feature and are not available in `.brs` files. diff --git a/docs/readme.md b/docs/readme.md index 7cb65d8fc..d4a13179b 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -14,6 +14,13 @@ sub doSomething() end sub ``` +## [Arrow Functions](arrow-functions.md) + +```brighterscript +' a short way to write a single-expression callback +adults = filter(people, (p) => p.age >= 18) +``` + ## [Assignments](assigments.md) ```brighterscript diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index cb80fb5b2..4a7a5a37c 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -988,6 +988,11 @@ export class CrossScopeValidator { } } + //an arrow function reaching for a local of its enclosing function gets a diagnostic that says why the name is unavailable + const arrowFunctionDiagnostic = util.getArrowFunctionCaptureDiagnostic(typeChainResult.astNode); + if (arrowFunctionDiagnostic) { + return arrowFunctionDiagnostic; + } if (isCallExpression(typeChainResult.astNode?.parent) && typeChainResult.astNode?.parent.callee === typeChainResult.astNode) { return DiagnosticMessages.cannotFindFunction(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor); } diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index b0291d526..40d74b21b 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -1401,6 +1401,30 @@ export let DiagnosticMessages = { message: `Function has ${count} labels used by goto, max is ${max}.`, severity: DiagnosticSeverity.Error, code: 'exceeds-max-goto-label-count' + }), + /** + * An arrow function references a local variable of the function it is written inside. Arrow functions are plain + * functions (closures are not supported yet, see #530), so that variable is not visible to them + * @param name the name of the variable being referenced + */ + arrowFunctionCannotCaptureVariable: (name: string) => ({ + message: `Arrow functions cannot use variable '${name}' from the enclosing function. Capturing outer variables is not supported yet, so pass it in as a parameter instead`, + severity: DiagnosticSeverity.Error, + code: 'arrow-function-cannot-capture-variable' + }), + /** + * `m` is not allowed inside arrow functions: what it should mean there (BrightScript's call-site `m` or a captured outer `m`) + * is left open until closures are designed, so allowing either now would lock that decision in + */ + arrowFunctionCannotUseM: () => ({ + message: `'m' cannot be used inside an arrow function. Pass the value in as a parameter instead`, + severity: DiagnosticSeverity.Error, + code: 'arrow-function-cannot-use-m' + }), + expectedExpressionAfterArrow: () => ({ + message: `Expected an expression after '=>'`, + severity: DiagnosticSeverity.Error, + code: 'expected-expression-after-arrow' }) }; export const defaultMaximumTruncationLength = 160; diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 854025816..be577769e 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -366,7 +366,8 @@ export class BrsFileValidator { // symbol table should not include any symbols from parent func funcSymbolTable.pushParentProvider(() => node.findAncestor(isBody).getSymbolTable()); } - if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) { + //arrow functions don't get an `m`: what it should mean there is left open until closures are designed (see #1929) + if (!node.isArrowFunction && (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc)) { if (!isTypecastStatement(node.body?.statements?.[0])) { // if this is an inline function, or if the function body does not start with a typecast statement, add the `m` symbol to the function scope. If the function body starts with a typecast statement, the `m` symbol will be added in the visitor for TypecastStatement, and it will be typed as the type from the typecast statement funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime); @@ -377,6 +378,9 @@ export class BrsFileValidator { this.collectFunctionLiterals(node); this.validateFunctionGotoLabelCount(node); this.flagControlFlowUnreachableCode(node); + if (node.isArrowFunction) { + this.validateArrowFunctionDoesNotUseM(node); + } }, FunctionParameterExpression: (node) => { if (isTypedFunctionTypeExpression(node.parent)) { @@ -459,6 +463,11 @@ export class BrsFileValidator { }, ReturnStatement: (node) => { const func = node.findAncestor(isFunctionExpression); + if (func?.isArrowFunction) { + //an arrow function's `return` is synthesized from its body expression so it always has a value; + //whether that value fits the return type is checked by ScopeValidator.validateReturnStatement + return; + } //these situations cannot have a value next to `return` if ( //`function as void`, `sub as void` @@ -753,6 +762,29 @@ export class BrsFileValidator { } } + /** + * `m` is not allowed inside arrow functions: what it should mean there (BrightScript's call-site `m` or a captured outer `m`) + * is left open until closures are designed (see #1929). Checked per arrow function rather than for every `m` in the file, so + * only arrow function bodies pay for it. Does not step into nested functions: a nested arrow function is checked on its own + * visit, and a nested regular function may use `m` as usual + */ + private validateArrowFunctionDoesNotUseM(arrow: FunctionExpression) { + const visitor = createVisitor({ + VariableExpression: (node) => { + if (node.tokens.name?.text?.toLowerCase() === 'm') { + this.event.program.diagnostics.register({ + ...DiagnosticMessages.arrowFunctionCannotUseM(), + location: node.tokens.name.location + }); + } + } + }); + for (const param of arrow.parameters) { + param.walk(visitor, { walkMode: WalkMode.visitExpressions }); + } + arrow.body?.walk(visitor, { walkMode: WalkMode.visitExpressions }); + } + private validateFunctionParameterCount(func: FunctionExpression) { if (func.parameters.length > CallExpression.MaximumArguments) { //flag every parameter over the limit diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 1a2404b66..35f906235 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -891,7 +891,13 @@ export class ScopeValidator { private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) { const data: ExtraSymbolData = {}; const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data }; - let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime }); + const func = returnStmt.findAncestor(isFunctionExpression); + let funcType = func?.getType({ flags: SymbolTypeFlag.typetime }); + if (func?.isArrowFunction && isTypedFunctionType(funcType) && isVoidType(funcType.returnType) && isCallExpression(returnStmt.value)) { + //a void arrow function (`(x) => list.push(x)`) transpiles to a `sub` that runs its body as a plain statement, + //so there is no return value to check + return; + } if (isTypedFunctionType(funcType)) { let actualReturnType = returnStmt?.value ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions) @@ -1271,7 +1277,13 @@ export class ScopeValidator { // only show "cannot find... " errors if the type is not defined from a doc comment const typeChainScan = util.processTypeChain(typeChain); const circularReferenceInfo = this.getCircularReference(exprType); - if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) { + const arrowFunctionDiagnostic = util.getArrowFunctionCaptureDiagnostic(typeChainScan.astNode); + if (arrowFunctionDiagnostic) { + this.addMultiScopeDiagnostic({ + ...arrowFunctionDiagnostic, + location: typeChainScan?.location + }); + } else if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)), location: typeChainScan?.location diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index 730886fc3..36ddc54ba 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -42,6 +42,36 @@ describe('lexer', () => { ]); }); + it('recognizes the arrow operator without disturbing equals and greater-than', () => { + expectKinds('=> = > >= = >', [ + TokenKind.Arrow, + TokenKind.Equal, + TokenKind.Greater, + TokenKind.GreaterEqual, + TokenKind.Equal, + TokenKind.Greater + ]); + }); + + it('recognizes the arrow operator without surrounding whitespace', () => { + expectKinds('(x)=>x*2', [ + TokenKind.LeftParen, + TokenKind.Identifier, + TokenKind.RightParen, + TokenKind.Arrow, + TokenKind.Identifier, + TokenKind.Star, + TokenKind.IntegerLiteral + ]); + }); + + it('gives the arrow token its text and location', () => { + const tokens = Lexer.scan('a => b').tokens; + const arrow = tokens.find(x => x.kind === TokenKind.Arrow); + expect(arrow.text).to.eql('=>'); + expect(arrow.location.range).to.eql(Range.create(0, 2, 0, 4)); + }); + it('separates optional chain characters and LeftSquare when found at beginning of statement locations', () => { //a statement starting with a question mark is actually a print statement, so we need to keep the ? separate from [ expectKinds(`?[ ?[ : ?[ ?[`, [ diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts index 548ac002b..00a0d716e 100644 --- a/src/lexer/Lexer.ts +++ b/src/lexer/Lexer.ts @@ -275,6 +275,14 @@ export class Lexer { break; } }, + '=': function (this: Lexer) { + if (this.peek() === '>') { + this.advance(); + this.addToken(TokenKind.Arrow); + } else { + this.addToken(TokenKind.Equal); + } + }, '>': function (this: Lexer) { switch (this.peek()) { case '=': @@ -348,7 +356,6 @@ export class Lexer { private static tokenKindMap = { '(': TokenKind.LeftParen, ')': TokenKind.RightParen, - '=': TokenKind.Equal, ',': TokenKind.Comma, '{': TokenKind.LeftCurlyBrace, '}': TokenKind.RightCurlyBrace, diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index 5e0b8e060..3dc83af3b 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -42,6 +42,7 @@ export enum TokenKind { GreaterEqual = 'GreaterEqual', // >= Equal = 'Equal', // = LessGreater = 'LessGreater', // BrightScript uses `<>` for "not equal" + Arrow = 'Arrow', // => (arrow function) // literals Identifier = 'Identifier', @@ -836,6 +837,7 @@ export const FixedTokenText: Partial> = { [TokenKind.GreaterEqual]: '>=', [TokenKind.Equal]: '=', [TokenKind.LessGreater]: '<>', + [TokenKind.Arrow]: '=>', [TokenKind.Dot]: '.', [TokenKind.DotDotDot]: '...', [TokenKind.Comma]: ',', diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 1a083ee73..6a8fe01a4 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -10,7 +10,7 @@ import { ParseMode } from './Parser'; import type { WalkOptions, WalkVisitor } from '../astUtils/visitors'; import { WalkMode } from '../astUtils/visitors'; import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors'; -import { isAAIndexedMemberExpression, isAALiteralExpression, isAAMemberExpression, isArrayLiteralExpression, isArrayType, isAssociativeArrayType, isCallableType, isCallExpression, isCallfuncExpression, isClassType, isDottedGetExpression, isEnumType, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isInterfaceMethodStatement, isInterfaceType, isInvalidType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNativeType, isNewExpression, isPrimitiveType, isReferenceType, isSpreadExpression, isStringType, isTemplateStringExpression, isTypecastExpression, isTypeStatementType, isUnaryExpression, isVariableExpression, isVoidType } from '../astUtils/reflection'; +import { isAAIndexedMemberExpression, isAALiteralExpression, isAAMemberExpression, isArrayLiteralExpression, isArrayType, isAssociativeArrayType, isCallableType, isCallExpression, isCallfuncExpression, isClassType, isDottedGetExpression, isEnumType, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isInterfaceMethodStatement, isInterfaceType, isInvalidType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNativeType, isNewExpression, isPrimitiveType, isReferenceType, isReturnStatement, isSpreadExpression, isStringType, isTemplateStringExpression, isTypecastExpression, isTypedFunctionType, isTypeStatementType, isUnaryExpression, isUninitializedType, isVariableExpression, isVoidType } from '../astUtils/reflection'; import type { GetTypeOptions, TranspileResult, TypedefProvider, ExtraSymbolData } from '../interfaces'; import { TypeChainEntry } from '../interfaces'; import { VoidType } from '../types/VoidType'; @@ -278,6 +278,11 @@ export class FunctionExpression extends Expression implements TypedefProvider { returnTypeExpression?: TypeExpression; body: Block; endFunctionType?: Token; + /** + * The `=>` of an arrow function (`(a, b) => a + b`). When set, this is an arrow function: it has no `function`/`end function` + * keywords and its body is a single synthesized `return` statement holding the body expression + */ + arrow?: Token; }) { super(); this.tokens = { @@ -285,7 +290,8 @@ export class FunctionExpression extends Expression implements TypedefProvider { leftParen: options.leftParen, rightParen: options.rightParen, as: options.as, - endFunctionType: options.endFunctionType + endFunctionType: options.endFunctionType, + arrow: options.arrow }; this.parameters = options.parameters ?? []; this.body = options.body; @@ -309,10 +315,63 @@ export class FunctionExpression extends Expression implements TypedefProvider { readonly leftParen?: Token; readonly rightParen?: Token; readonly as?: Token; + readonly arrow?: Token; }; + /** + * Is this an arrow function (`(a, b) => a + b`)? Arrow functions are ordinary functions whose body is a single expression, + * stored as a synthesized `return` statement so hover, validation and transpile treat them like any other function + */ + public get isArrowFunction() { + return !!this.tokens.arrow; + } + + /** + * The expression that forms the body of an arrow function (the `a + b` in `(a, b) => a + b`). Undefined for regular functions + */ + public get arrowBody(): Expression | undefined { + const statement = this.isArrowFunction ? this.body?.statements?.[0] : undefined; + return isReturnStatement(statement) ? statement.value : undefined; + } + + /** + * For an arrow function with no declared return type: the type its body expression produces, or `void` when the body + * is a call to something that returns nothing (a `sub`, or a built-in like `push`). Undefined for regular functions + * and for arrow functions with a declared return type + */ + private getInferredArrowReturnType(): BscType | undefined { + const body = this.arrowBody; + if (!body || this.returnTypeExpression) { + return undefined; + } + //a call to a user-defined sub evaluates to `invalid` at runtime (so that's its type), but it still produces nothing worth returning + if (isCallExpression(body)) { + const calleeType = body.callee.getType({ flags: SymbolTypeFlag.runtime }); + if (isTypedFunctionType(calleeType) && (calleeType.isSub || isVoidType(calleeType.returnType))) { + return VoidType.instance; + } + } + const bodyType = body.getType({ flags: SymbolTypeFlag.runtime }); + return isUninitializedType(bodyType) ? VoidType.instance : bodyType; + } + + /** + * Does this arrow function produce no value? True when its return type is explicitly `void`, or when it has no declared + * return type and its body is a call to something that returns nothing (i.e. `(x) => list.push(x)`). + * These transpile to a `sub` whose body is a plain statement, since `return list.push(x)` fails on device + */ + private get isVoidArrowFunction() { + if (!this.isArrowFunction) { + return false; + } + const returnType = this.returnTypeExpression + ? this.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }) + : this.getInferredArrowReturnType(); + return isVoidType(returnType); + } + public get leadingTrivia(): Token[] { - return this.tokens.functionType?.leadingTrivia; + return this.tokens.functionType?.leadingTrivia ?? this.tokens.leftParen?.leadingTrivia; } public get endTrivia(): Token[] { @@ -321,7 +380,7 @@ export class FunctionExpression extends Expression implements TypedefProvider { /** * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing), - * and ending with the last n' in 'end function' or 'b' in 'end sub' + * and ending with the last n' in 'end function' or 'b' in 'end sub' (or the end of the body expression for an arrow function) */ public get location(): Location { return util.createBoundingLocation( @@ -331,15 +390,22 @@ export class FunctionExpression extends Expression implements TypedefProvider { this.tokens.rightParen, this.tokens.as, this.returnTypeExpression, + this.tokens.arrow, + this.isArrowFunction ? this.body : undefined, this.tokens.endFunctionType ); } transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) { let results = [] as TranspileResult; + let functionTypeText = this.tokens.functionType?.text; + if (this.isArrowFunction) { + //an arrow function has no keyword of its own, so pick one based on whether its body produces a value + functionTypeText = this.isVoidArrowFunction ? 'sub' : 'function'; + } //'function'|'sub' results.push( - state.transpileToken(this.tokens.functionType, 'function', false, state.skipLeadingComments) + state.transpileToken(this.tokens.functionType, functionTypeText ?? 'function', false, state.skipLeadingComments) ); //functionName? if (name) { @@ -381,7 +447,15 @@ export class FunctionExpression extends Expression implements TypedefProvider { let hasBody = false; if (includeBody) { state.lineage.unshift(this); - let body = this.body.transpile(state); + let body: TranspileResult; + if (this.isArrowFunction && functionTypeText === 'sub') { + //a `sub` can't `return` a value, so emit the body expression as a plain statement + state.blockDepth++; + body = [state.newline, state.indent(), ...this.arrowBody.transpile(state)]; + state.blockDepth--; + } else { + body = this.body.transpile(state); + } hasBody = body.length > 0; state.lineage.shift(); results.push(...body); @@ -389,7 +463,7 @@ export class FunctionExpression extends Expression implements TypedefProvider { const lastLocatable = hasBody ? this.body : this.returnTypeExpression ?? this.tokens.leftParen ?? this.tokens.functionType; results.push( - ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${this.tokens.functionType ?? 'function'}`) + ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${functionTypeText ?? 'function'}`) ); return results; } @@ -455,7 +529,14 @@ export class FunctionExpression extends Expression implements TypedefProvider { options ); - const isSub = this.tokens.functionType?.kind === TokenKind.Sub; + let isSub = this.tokens.functionType?.kind === TokenKind.Sub; + if (this.isArrowFunction && !returnType) { + //an arrow function with no declared return type returns whatever its body expression evaluates to + returnType = this.getInferredArrowReturnType(); + if (isVoidType(returnType)) { + isSub = true; + } + } //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic` if (!returnType) { returnType = isSub ? VoidType.instance : DynamicType.instance; @@ -530,7 +611,8 @@ export class FunctionExpression extends Expression implements TypedefProvider { leftParen: util.cloneToken(this.tokens.leftParen), rightParen: util.cloneToken(this.tokens.rightParen), as: util.cloneToken(this.tokens.as), - returnTypeExpression: this.returnTypeExpression?.clone() + returnTypeExpression: this.returnTypeExpression?.clone(), + arrow: util.cloneToken(this.tokens.arrow) }), ['body', 'returnTypeExpression'] ); diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 1b8e4f40a..66758d57b 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -1002,16 +1002,7 @@ export class Parser { [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression(); } - params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => { - if (haveFoundOptional && !param.defaultValue) { - this.diagnostics.push({ - ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text), - location: param.location - }); - } - - return haveFoundOptional || !!param.defaultValue; - }, false); + this.validateParameterOrder(params); this.consumeStatementSeparators(true); @@ -1113,6 +1104,22 @@ export class Parser { }); } + /** + * Flag required parameters that follow optional ones (i.e. `sub foo(a = 1, b)`), which is not allowed + */ + private validateParameterOrder(params: FunctionParameterExpression[]) { + params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => { + if (haveFoundOptional && !param.defaultValue) { + this.diagnostics.push({ + ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text), + location: param.location + }); + } + + return haveFoundOptional || !!param.defaultValue; + }, false); + } + private assignment(allowTypedAssignment = false): AssignmentStatement { let name = this.advance() as Identifier; //add diagnostic if name is a reserved word that cannot be used as an identifier @@ -3162,6 +3169,132 @@ export class Parser { } } + /** + * Tokens that end the line an arrow function's parameter list is on. `(` and `=>` must be on the same line, so hitting + * one of these while scanning means the parens are not an arrow function's + */ + private static arrowScanTerminators = new Set([TokenKind.Newline, TokenKind.Colon, TokenKind.Comment, TokenKind.Eof]); + + /** + * Does an arrow function (`(a, b) => a + b`) start at the current `(`? Decided by scanning tokens rather than by + * speculatively parsing, since `=>` is a single token that never appears inside a parameter list or a type expression + * and has to be on the same line as the closing `)`: find the matching `)`, then look for `=>` either right after it + * or after an `as ` clause. Costs a handful of token reads and no allocations, so every grouping expression + * (`(a + b)`, `(a as integer)`) can afford it + */ + private isArrowFunctionAhead(): boolean { + //find the `)` that matches the `(` at the current position + let index = this.current; + let depth = 0; + for (; index < this.tokens.length; index++) { + const kind = this.tokens[index].kind; + if (kind === TokenKind.LeftParen || kind === TokenKind.QuestionLeftParen) { + depth++; + } else if (kind === TokenKind.RightParen) { + depth--; + if (depth === 0) { + break; + } + } else if (Parser.arrowScanTerminators.has(kind)) { + return false; + } + } + if (depth !== 0) { + return false; + } + index++; + if (this.tokens[index]?.kind === TokenKind.Arrow) { + return true; + } + if (this.tokens[index]?.kind !== TokenKind.As) { + return false; + } + //`) as =>`: the return type may contain brackets of its own (function types, typed arrays, inline interfaces), + //so only a `=>` outside of them counts, and a `,` or an unmatched closing bracket outside of them ends the type + depth = 0; + for (index++; index < this.tokens.length; index++) { + const kind = this.tokens[index].kind; + if (kind === TokenKind.LeftParen || kind === TokenKind.LeftSquareBracket || kind === TokenKind.LeftCurlyBrace) { + depth++; + } else if (kind === TokenKind.RightParen || kind === TokenKind.RightSquareBracket || kind === TokenKind.RightCurlyBrace) { + if (depth === 0) { + return false; + } + depth--; + } else if (depth === 0 && kind === TokenKind.Arrow) { + return true; + } else if ((depth === 0 && kind === TokenKind.Comma) || Parser.arrowScanTerminators.has(kind)) { + return false; + } + } + return false; + } + + /** + * Parse an arrow function (`(a, b) => a + b`) when one starts at the current `(`, otherwise return undefined without + * consuming anything so the caller parses the parens as a grouping expression (`(a + b)`, `(a as integer)`) instead + */ + private tryArrowFunction(): FunctionExpression | undefined { + if (!this.isArrowFunctionAhead()) { + return undefined; + } + //like `functionDeclaration`, an arrow function is its own world for call tracking and nesting depth + const previousCallExpressions = this.callExpressions; + this.callExpressions = []; + this.namespaceAndFunctionDepth++; + try { + const leftParen = this.advance(); + const params = [] as FunctionParameterExpression[]; + if (!this.check(TokenKind.RightParen)) { + do { + params.push(this.functionParameter()); + } while (this.match(TokenKind.Comma)); + } + const rightParen = this.consume( + DiagnosticMessages.unmatchedLeftToken(leftParen.text, 'function parameter list'), + TokenKind.RightParen + ); + let asToken: Token; + let returnTypeExpression: TypeExpression; + if (this.check(TokenKind.As)) { + [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression(); + } + const arrow = this.consume(DiagnosticMessages.expectedToken(TokenKind.Arrow), TokenKind.Arrow); + if (this.options.mode !== ParseMode.BrighterScript) { + this.diagnostics.push({ + ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('arrow functions'), + location: arrow.location + }); + } + this.validateParameterOrder(params); + + const value = this.checkEndOfStatement() ? undefined : this.expression(); + if (!value) { + this.diagnostics.push({ + ...DiagnosticMessages.expectedExpressionAfterArrow(), + location: arrow.location + }); + throw this.lastDiagnosticAsError(); + } + //store the body as a `return` statement so the rest of the compiler treats this like any other function + const body = new Block({ + statements: [new ReturnStatement({ value: value })] + }); + return new FunctionExpression({ + leftParen: leftParen, + parameters: params, + rightParen: rightParen, + as: asToken, + returnTypeExpression: returnTypeExpression, + arrow: arrow, + body: body + }); + } finally { + this.namespaceAndFunctionDepth--; + this.callExpressions = previousCallExpressions; + } + } + private boolean(): Expression { let expr = this.relational(); @@ -3731,6 +3864,13 @@ export class Parser { } private primary(): Expression { + //`(` might start an arrow function (`(a, b) => a + b`) rather than a grouping expression + if (this.check(TokenKind.LeftParen)) { + const arrowFunction = this.tryArrowFunction(); + if (arrowFunction) { + return arrowFunction; + } + } switch (true) { case this.matchAny( TokenKind.False, diff --git a/src/parser/tests/expression/ArrowFunction.spec.ts b/src/parser/tests/expression/ArrowFunction.spec.ts new file mode 100644 index 000000000..41397ac63 --- /dev/null +++ b/src/parser/tests/expression/ArrowFunction.spec.ts @@ -0,0 +1,1738 @@ +import { expect } from '../../../chai-config.spec'; +import { Range } from 'vscode-languageserver'; +import { DiagnosticMessages } from '../../../DiagnosticMessages'; +import { Program } from '../../../Program'; +import { expectDiagnostics, expectZeroDiagnostics, getTestGetTypedef, getTestTranspile, rootDir } from '../../../testHelpers.spec'; +import { ParseMode, Parser } from '../../Parser'; +import { isAALiteralExpression, isArrayLiteralExpression, isBinaryExpression, isCallExpression, isFunctionExpression, isGroupingExpression, isLiteralExpression, isPrintStatement, isReturnStatement, isTernaryExpression, isTypecastExpression, isVoidType } from '../../../astUtils/reflection'; +import { WalkMode } from '../../../astUtils/visitors'; +import type { AssignmentStatement, FunctionStatement, PrintStatement, ReturnStatement } from '../../Statement'; +import type { AALiteralExpression, AAMemberExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, FunctionExpression, GroupingExpression } from '../../Expression'; +import type { AstNode } from '../../AstNode'; +import { SymbolTypeFlag } from '../../../SymbolTypeFlag'; +import type { BrsFile } from '../../../files/BrsFile'; +import { TokenKind } from '../../../lexer/TokenKind'; +import { util } from '../../../util'; + +describe('arrow functions', () => { + /** + * Parse `code` as the body of a function and return the parsed statements + */ + function parseBody(code: string, mode = ParseMode.BrighterScript) { + const parser = Parser.parse(`function main()\n ${code}\nend function`, { mode: mode }); + expectZeroDiagnostics(parser.diagnostics); + return (parser.ast.statements[0] as FunctionStatement).func.body.statements; + } + + /** + * Parse `code` as the only statement of a function body and return the value of its assignment + */ + function parseValue(code: string) { + const assignment = parseBody(code)[0] as AssignmentStatement; + return assignment.value; + } + + function expectArrow(value: AstNode) { + expect(isFunctionExpression(value), 'expected an arrow function').to.be.true; + const func = value as FunctionExpression; + expect(func.isArrowFunction).to.be.true; + return func; + } + + function parseArrow(code: string) { + return expectArrow(parseValue(code)); + } + + describe('parser', () => { + describe('forms', () => { + it('parses an arrow function with no params', () => { + const func = parseArrow('getZero = () => 0'); + expect(func.parameters).to.be.empty; + expect(isLiteralExpression(func.arrowBody)).to.be.true; + //the body is stored as a `return` statement, and there are no function keywords + expect(func.body.statements).to.have.lengthOf(1); + expect(isReturnStatement(func.body.statements[0])).to.be.true; + expect(func.tokens.functionType).to.be.undefined; + expect(func.tokens.endFunctionType).to.be.undefined; + expect(func.tokens.arrow.kind).to.eql(TokenKind.Arrow); + }); + + it('parses a single param', () => { + const func = parseArrow('twice = (x) => x * 2'); + expect(func.parameters.map(p => p.tokens.name.text)).to.eql(['x']); + expect(isBinaryExpression(func.arrowBody)).to.be.true; + }); + + it('parses multiple untyped params', () => { + const func = parseArrow('add = (a, b, c) => a + b + c'); + expect(func.parameters.map(p => p.tokens.name.text)).to.eql(['a', 'b', 'c']); + }); + + it('parses typed params and a return type', () => { + const func = parseArrow('add = (a as integer, b as integer) as integer => a + b'); + expect(func.parameters.map(p => p.tokens.name.text)).to.eql(['a', 'b']); + expect(func.parameters[0].typeExpression.getType({ flags: SymbolTypeFlag.typetime }).toString()).to.eql('integer'); + expect(func.tokens.as).to.exist; + expect(func.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }).toString()).to.eql('integer'); + }); + + it('parses a mix of typed and untyped params', () => { + const func = parseArrow('f = (a, b as string, c) => a'); + expect(func.parameters[0].typeExpression).to.be.undefined; + expect(func.parameters[1].typeExpression).to.exist; + expect(func.parameters[2].typeExpression).to.be.undefined; + }); + + it('parses typed array params and union return types', () => { + const func = parseArrow('first = (items as integer[]) as integer or string => items[0]'); + expect(func.parameters[0].typeExpression.getType({ flags: SymbolTypeFlag.typetime }).toString()).to.eql('Array'); + expect(func.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }).toString()).to.eql('integer or string'); + }); + + it('parses function-typed params', () => { + const func = parseArrow('apply = (callback as function(x as integer) as integer) => callback(1)'); + expect(func.parameters[0].typeExpression.getType({ flags: SymbolTypeFlag.typetime }).toString()).to.eql('function (x as integer) as integer'); + }); + + it('parses a void return type', () => { + const func = parseArrow('logIt = (message as string) as void => doLog(message)'); + expect(isVoidType(func.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }))).to.be.true; + }); + + it('parses default param values', () => { + const func = parseArrow('greet = (name = "world") => "hello " + name'); + expect(func.parameters[0].defaultValue).to.exist; + expect(func.parameters[0].tokens.equals).to.exist; + }); + + it('parses a default value that refers to an earlier param', () => { + const func = parseArrow('f = (a, b = a) => a + b'); + expect(func.parameters[1].defaultValue).to.exist; + }); + + it('parses an arrow function as a default param value', () => { + const func = parseArrow('f = (callback = (x) => x) => callback(1)'); + expectArrow(func.parameters[0].defaultValue); + }); + + it('parses without any whitespace', () => { + const func = parseArrow('f=(x)=>x*2'); + expect(func.parameters.map(p => p.tokens.name.text)).to.eql(['x']); + expect(isBinaryExpression(func.arrowBody)).to.be.true; + }); + + it('parses with extra whitespace', () => { + const func = parseArrow('f = ( x , y ) => x'); + expect(func.parameters.map(p => p.tokens.name.text)).to.eql(['x', 'y']); + }); + + it('stops the body at a colon statement separator', () => { + const statements = parseBody('f = (x) => x : print f(1)'); + expect(statements).to.have.lengthOf(2); + expectArrow((statements[0] as AssignmentStatement).value); + expect(isPrintStatement(statements[1])).to.be.true; + }); + }); + + describe('body', () => { + it('parses a parenthesized AA literal body', () => { + const func = parseArrow('makePoint = (x, y) => ({ x: x, y: y })'); + expect(isGroupingExpression(func.arrowBody)).to.be.true; + expect(isAALiteralExpression((func.arrowBody as GroupingExpression).expression)).to.be.true; + }); + + it('parses an array literal body', () => { + const func = parseArrow('pair = (x, y) => [x, y]'); + expect(isArrayLiteralExpression(func.arrowBody)).to.be.true; + }); + + it('parses a body that starts with a grouping expression', () => { + const func = parseArrow('f = (x) => (x + 1) * 2'); + expect(isBinaryExpression(func.arrowBody)).to.be.true; + expect(isGroupingExpression((func.arrowBody as BinaryExpression).left)).to.be.true; + }); + + it('extends the body as far right as possible', () => { + const func = parseArrow('pick = (a) => a ? 1 : 2'); + expect(isTernaryExpression(func.arrowBody)).to.be.true; + }); + + it('parses a call body', () => { + const func = parseArrow('f = (x) => doThing(x, 1)'); + expect(isCallExpression(func.arrowBody)).to.be.true; + }); + + it('parses a typecast body', () => { + const func = parseArrow('f = (x) => x as string'); + expect(isTypecastExpression(func.arrowBody)).to.be.true; + }); + + it('parses nested arrow functions', () => { + const outer = parseArrow('f = (a) => (b) => b'); + const inner = expectArrow(outer.arrowBody); + expect(inner.parameters.map(p => p.tokens.name.text)).to.eql(['b']); + }); + + it('parses a body that immediately invokes another arrow function', () => { + const func = parseArrow('f = (x) => ((y) => y * 2)(x)'); + expect(isCallExpression(func.arrowBody)).to.be.true; + const callee = (func.arrowBody as CallExpression).callee; + expect(isGroupingExpression(callee)).to.be.true; + expectArrow((callee as GroupingExpression).expression); + }); + }); + + describe('contexts', () => { + it('parses an arrow function as a call argument', () => { + const value = parseValue('result = filter(items, (i) => i.rating > 3.5)'); + expect(isCallExpression(value)).to.be.true; + expectArrow((value as CallExpression).args[1]); + }); + + it('parses several arrow functions in one call', () => { + const value = parseValue('result = combine((a) => a, (b) => b, (c) => c)') as CallExpression; + expect(value.args).to.have.lengthOf(3); + for (const arg of value.args) { + expectArrow(arg); + } + }); + + it('parses an immediately-invoked arrow function', () => { + const value = parseValue('y = ((a, b) => a + b)(1, 2)'); + expect(isCallExpression(value)).to.be.true; + const callee = (value as CallExpression).callee; + expect(isGroupingExpression(callee)).to.be.true; + expectArrow((callee as GroupingExpression).expression); + expect((value as CallExpression).args).to.have.lengthOf(2); + }); + + it('parses arrow functions inside an array literal', () => { + const value = parseValue('arr = [(x) => x, (y) => y * 2]') as ArrayLiteralExpression; + expect(value.elements).to.have.lengthOf(2); + expectArrow(value.elements[0]); + expectArrow(value.elements[1]); + }); + + it('parses an arrow function as an AA literal value', () => { + const value = parseValue('aa = { callback: (x) => x }') as AALiteralExpression; + expectArrow((value.elements[0] as AAMemberExpression).value); + }); + + it('parses an arrow function in a return statement', () => { + const statement = parseBody('return (z) => z')[0] as ReturnStatement; + expect(isReturnStatement(statement)).to.be.true; + expectArrow(statement.value); + }); + + it('parses an immediately-invoked arrow function in a print statement', () => { + const statement = parseBody('print ((x) => x * 2)(4)')[0] as PrintStatement; + expect(isPrintStatement(statement)).to.be.true; + expect(isCallExpression(statement.expressions[0])).to.be.true; + }); + + it('parses an arrow function as a class field initializer', () => { + const parser = Parser.parse(` + class Person + callback = (x as integer) => x + end class + `, { mode: ParseMode.BrighterScript }); + expectZeroDiagnostics(parser.diagnostics); + const field = (parser.ast.statements[0] as any).fields[0]; + expectArrow(field.initialValue); + }); + + it('parses an arrow function inside a namespace', () => { + const parser = Parser.parse(` + namespace alpha + sub main() + f = (x) => beta(x) + end sub + end namespace + `, { mode: ParseMode.BrighterScript }); + expectZeroDiagnostics(parser.diagnostics); + const func = parser.ast.findChild((node) => isFunctionExpression(node) && node.isArrowFunction, { walkMode: WalkMode.visitAllRecursive }); + expect(func).to.exist; + }); + }); + + describe('does not disturb existing syntax', () => { + it('still parses grouping expressions', () => { + const value = parseValue('x = (a + b) * 2'); + expect(isBinaryExpression(value)).to.be.true; + expect(isGroupingExpression((value as BinaryExpression).left)).to.be.true; + }); + + it('still parses a parenthesized variable', () => { + const value = parseValue('x = (a)'); + expect(isGroupingExpression(value)).to.be.true; + }); + + it('still parses a parenthesized typecast', () => { + const value = parseValue('x = (a as integer)'); + expect(isGroupingExpression(value)).to.be.true; + expect(isTypecastExpression((value as GroupingExpression).expression)).to.be.true; + }); + + it('still parses a parenthesized typecast followed by more expression', () => { + const value = parseValue('x = (a as integer) + 1'); + expect(isBinaryExpression(value)).to.be.true; + expect(isGroupingExpression((value as BinaryExpression).left)).to.be.true; + }); + + it('still parses a parenthesized comparison', () => { + const value = parseValue('x = (a = 1)'); + expect(isGroupingExpression(value)).to.be.true; + expect(isBinaryExpression((value as GroupingExpression).expression)).to.be.true; + }); + + it('still parses a parenthesized call', () => { + const value = parseValue('x = (a(1))'); + expect(isGroupingExpression(value)).to.be.true; + expect(isCallExpression((value as GroupingExpression).expression)).to.be.true; + }); + + it('still parses a parenthesized condition in an if statement', () => { + const statements = parseBody('if (a) then print 1'); + expect(statements).to.have.lengthOf(1); + }); + + it('still parses a greater-than-or-equal comparison', () => { + const value = parseValue('x = a >= b') as BinaryExpression; + expect(isBinaryExpression(value)).to.be.true; + expect(value.tokens.operator.kind).to.eql(TokenKind.GreaterEqual); + }); + + it('still parses regular anonymous functions', () => { + const statements = parseBody('f = function(x)\n return x\n end function'); + const func = (statements[0] as AssignmentStatement).value as FunctionExpression; + expect(isFunctionExpression(func)).to.be.true; + expect(func.isArrowFunction).to.be.false; + expect(func.arrowBody).to.be.undefined; + }); + }); + + describe('locations', () => { + it('spans from the open paren to the end of the body', () => { + const func = parseArrow('twice = (x) => x * 2'); + // twice = (x) => x * 2 + expect(func.location.range).to.eql(Range.create(1, 12, 1, 24)); + }); + + it('spans a typed arrow function', () => { + const func = parseArrow('add = (a as integer, b as integer) as integer => a + b'); + // add = (a as integer, b as integer) as integer => a + b + expect(func.location.range).to.eql(Range.create(1, 10, 1, 58)); + }); + + it('locates the arrow token', () => { + const func = parseArrow('twice = (x) => x * 2'); + expect(func.tokens.arrow.text).to.eql('=>'); + expect(func.tokens.arrow.location.range).to.eql(Range.create(1, 16, 1, 18)); + }); + + it('locates the body expression', () => { + const func = parseArrow('twice = (x) => x * 2'); + expect(func.arrowBody.location.range).to.eql(Range.create(1, 19, 1, 24)); + //the synthesized return statement has the same range as the body (there is no `return` token) + expect(func.body.statements[0].location.range).to.eql(Range.create(1, 19, 1, 24)); + }); + }); + + describe('ast', () => { + it('links parents', () => { + const func = parseArrow('twice = (x) => x * 2'); + expect(func.body.parent).to.equal(func); + expect(func.body.statements[0].parent).to.equal(func.body); + expect(func.arrowBody.parent).to.equal(func.body.statements[0]); + expect(func.parameters[0].parent).to.equal(func); + }); + + it('is found by recursive walks, including its synthesized return statement', () => { + const parser = Parser.parse(`sub main()\n f = (x) => x\nend sub`, { mode: ParseMode.BrighterScript }); + const arrows = parser.ast.findChildren((node) => isFunctionExpression(node) && node.isArrowFunction, { walkMode: WalkMode.visitAllRecursive }); + expect(arrows).to.have.lengthOf(1); + const returns = parser.ast.findChildren(isReturnStatement, { walkMode: WalkMode.visitAllRecursive }); + expect(returns).to.have.lengthOf(1); + }); + + it('clones', () => { + const func = parseArrow('twice = (x) => x * 2'); + const clone = func.clone(); + expect(clone.isArrowFunction).to.be.true; + expect(clone.arrowBody).to.exist; + expect(clone.arrowBody).not.to.equal(func.arrowBody); + expect(clone.tokens.arrow).not.to.equal(func.tokens.arrow); + expect(clone.tokens.arrow.text).to.eql('=>'); + expect(clone.body.parent).to.equal(clone); + expect(clone.location.range).to.eql(func.location.range); + }); + }); + + describe('diagnostics', () => { + it('flags a missing body before a newline', () => { + const parser = Parser.parse(`sub main()\n x = (a) =>\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.expectedExpressionAfterArrow() + ]); + }); + + it('flags a missing body before a colon', () => { + const parser = Parser.parse(`sub main()\n x = (a) => : print 1\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.expectedExpressionAfterArrow() + ]); + }); + + it('flags a missing body at the end of the file', () => { + const parser = Parser.parse(`x = (a) =>`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.expectedExpressionAfterArrow() + ]); + }); + + it('flags required params after optional ones', () => { + const parser = Parser.parse(`sub main()\n f = (a = 1, b) => a + b\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter('b') + ]); + }); + + it('flags a missing type after "as" but still parses the arrow function', () => { + const parser = Parser.parse(`sub main()\n f = (a as) => a\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.expectedIdentifier('as') + ]); + const func = parser.ast.findChild((node) => isFunctionExpression(node) && node.isArrowFunction, { walkMode: WalkMode.visitAllRecursive }); + expect(func).to.exist; + }); + + it('reports an unmatched paren the same as a grouping expression would', () => { + const parser = Parser.parse(`sub main()\n f = (a, b => a\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.unmatchedLeftToken('(', 'expression') + ]); + }); + + it('flags a standalone arrow function statement', () => { + const parser = Parser.parse(`sub main()\n (x) => x\nend sub`, { mode: ParseMode.BrighterScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.expectedStatement() + ]); + }); + + it('is not allowed in brightscript mode', () => { + const parser = Parser.parse(`sub main()\n f = (a) => a\nend sub`, { mode: ParseMode.BrightScript }); + expectDiagnostics(parser.diagnostics, [ + DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('arrow functions') + ]); + }); + + it('still parses groupings in brightscript mode without diagnostics', () => { + const parser = Parser.parse(`sub main()\n x = (a + b) * 2\n y = (a)\nend sub`, { mode: ParseMode.BrightScript }); + expectZeroDiagnostics(parser.diagnostics); + }); + }); + }); + + describe('program', () => { + let program: Program; + const testTranspile = getTestTranspile(() => [program, rootDir]); + const testGetTypedef = getTestGetTypedef(() => [program, rootDir]); + + beforeEach(() => { + program = new Program({ rootDir: rootDir, sourceMap: true }); + }); + afterEach(() => { + program.dispose(); + }); + + /** + * Validate `source/main.bs` containing `code` (expecting no diagnostics) and return the first arrow function in it + */ + function getArrowFunction(code: string) { + const file = program.setFile('source/main.bs', code); + program.validate(); + expectZeroDiagnostics(program); + return file.ast.findChild((node) => isFunctionExpression(node) && node.isArrowFunction, { walkMode: WalkMode.visitAllRecursive }); + } + + /** + * Validate `source/main.bs` containing `code` and return the codes of the resulting diagnostics + */ + function getDiagnosticCodes(code: string) { + program.setFile('source/main.bs', code); + program.validate(); + return program.getDiagnostics().map(x => x.code); + } + + describe('transpile', () => { + it('transpiles to an anonymous function that returns the expression', async () => { + await testTranspile(` + sub main() + twice = (x) => x * 2 + end sub + `, ` + sub main() + twice = function(x) + return x * 2 + end function + end sub + `); + }); + + it('transpiles an arrow function with no params', async () => { + await testTranspile(` + sub main() + getZero = () => 0 + end sub + `, ` + sub main() + getZero = function() + return 0 + end function + end sub + `); + }); + + it('keeps param and return types', async () => { + await testTranspile(` + sub main() + add = (a as integer, b as integer) as integer => a + b + end sub + `, ` + sub main() + add = function(a as integer, b as integer) as integer + return a + b + end function + end sub + `); + }); + + it('removes param and return types when removeParameterTypes is set', async () => { + program.options.removeParameterTypes = true; + await testTranspile(` + sub main() + add = (a as integer, b as integer) as integer => a + b + end sub + `, ` + sub main() + add = function(a, b) + return a + b + end function + end sub + `); + }); + + it('converts brighterscript-only param types', async () => { + await testTranspile(` + class Person + name as string + end class + sub main() + getName = (p as Person, others as Person[]) => p.name + end sub + `, ` + sub __Person_method_new() + m.name = invalid + end sub + function __Person_builder() + instance = {} + instance.new = __Person_method_new + return instance + end function + function Person() + instance = __Person_builder() + instance.new() + return instance + end function + + sub main() + getName = function(p as dynamic, others as dynamic) + return p.name + end function + end sub + `); + }); + + it('keeps default param values', async () => { + await testTranspile(` + sub main() + greet = (name = "world") => "hello " + name + end sub + `, ` + sub main() + greet = function(name = "world") + return "hello " + name + end function + end sub + `); + }); + + it('keeps a default value that refers to an earlier param', async () => { + await testTranspile(` + sub main() + f = (a, b = a) => a + b + end sub + `, ` + sub main() + f = function(a, b = a) + return a + b + end function + end sub + `); + }); + + it('transpiles as a call argument', async () => { + await testTranspile(` + sub main() + result = apply((i) => i > 1) + end sub + function apply(predicate) + return predicate(2) + end function + `, ` + sub main() + result = apply(function(i) + return i > 1 + end function) + end sub + + function apply(predicate) + return predicate(2) + end function + `); + }); + + it('transpiles several arrow functions in one call', async () => { + await testTranspile(` + sub main() + result = combine((a) => a, (b) => b) + end sub + function combine(first, second) + return first(1) + second(2) + end function + `, ` + sub main() + result = combine(function(a) + return a + end function, function(b) + return b + end function) + end sub + + function combine(first, second) + return first(1) + second(2) + end function + `); + }); + + it('transpiles nested arrow functions', async () => { + await testTranspile(` + sub main() + f = (a) => (b) => b + end sub + `, ` + sub main() + f = function(a) + return function(b) + return b + end function + end function + end sub + `); + }); + + it('transpiles immediately-invoked arrow functions', async () => { + await testTranspile(` + sub main() + print ((x) => x * 2)(4) + y = ((a, b) => a + b)(1, 2) + end sub + `, ` + sub main() + print (function(x) + return x * 2 + end function)(4) + y = (function(a, b) + return a + b + end function)(1, 2) + end sub + `); + }); + + it('transpiles a parenthesized AA literal body', async () => { + await testTranspile(` + sub main() + makePoint = (x, y) => ({ x: x, y: y }) + end sub + `, ` + sub main() + makePoint = function(x, y) + return ({ + x: x + y: y + }) + end function + end sub + `); + }); + + it('transpiles a template string body', async () => { + await testTranspile(` + sub main() + greet = (name) => \`hi \${name}\` + end sub + `, ` + sub main() + greet = function(name) + return ("hi " + bslib_toString(name)) + end function + end sub + `); + }); + + it('transpiles a null-coalescing body', async () => { + await testTranspile(` + sub main() + orOne = (x) => x ?? 1 + end sub + `, ` + sub main() + orOne = function(x) + return bslib_coalesce(x, 1) + end function + end sub + `); + }); + + it('transpiles a ternary body', async () => { + await testTranspile(` + sub main() + size = (x) => x > 1 ? "big" : "small" + end sub + `, ` + sub main() + size = function(x) + return bslib_ternary(x > 1, "big", "small") + end function + end sub + `); + }); + + it('transpiles arrow functions as ternary branches', async () => { + await testTranspile(` + sub main(useA) + callback = useA ? (a) => a : (b) => b + end sub + `, ` + sub main(useA) + if useA then + callback = function(a) + return a + end function + else + callback = function(b) + return b + end function + end if + end sub + `); + }); + + it('transpiles an arrow function as a null-coalescing fallback', async () => { + await testTranspile(` + sub main(handler) + callback = handler ?? ((x) => x) + end sub + `, ` + sub main(handler) + callback = bslib_coalesce(handler, (function(x) + return x + end function)) + end sub + `); + }); + + it('transpiles arrow functions inside array literals, AA literals and return statements', async () => { + await testTranspile(` + function main() + arr = [(x) => x, (y) => y * 2] + aa = { callback: (x) => x } + return (z) => z + end function + `, ` + function main() + arr = [ + function(x) + return x + end function + function(y) + return y * 2 + end function + ] + aa = { + callback: function(x) + return x + end function + } + return function(z) + return z + end function + end function + `); + }); + + it('transpiles an arrow function as a callfunc argument', async () => { + await testTranspile(` + sub main(node as roSGNode) + node@.doIt((x) => x) + end sub + `, ` + sub main(node as dynamic) + node.callfunc("doIt", function(x) + return x + end function) + end sub + `); + }); + + it('resolves namespace-relative references inside the body', async () => { + await testTranspile(` + namespace alpha + function beta(x) + return x + end function + sub main() + f = (x) => beta(x) + g = (x) => alpha.beta(x) + end sub + end namespace + `, ` + function alpha_beta(x) + return x + end function + + sub alpha_main() + f = function(x) + return alpha_beta(x) + end function + g = function(x) + return alpha_beta(x) + end function + end sub + `); + }); + + it('inlines enums and consts used inside the body', async () => { + program.setFile('source/main.bs', ` + enum Direction + up = 1 + end enum + const MAX = 10 + sub main() + f = (x) => x + Direction.up + MAX + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const result = await program.getTranspiledFileContents('source/main.bs'); + expect(result.code).to.include('return x + 1 + 10'); + }); + + it('transpiles class constructors called inside the body', async () => { + program.setFile('source/main.bs', ` + class Person + end class + sub main() + make = () => new Person() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const result = await program.getTranspiledFileContents('source/main.bs'); + expect(result.code).to.include('make = function()\n return Person()\n end function'); + }); + + it('transpiles an arrow function as a class field initializer', async () => { + program.setFile('source/main.bs', ` + class Person + callback = (x as integer) => x + end class + `); + program.validate(); + expectZeroDiagnostics(program); + const result = await program.getTranspiledFileContents('source/main.bs'); + expect(result.code).to.include('m.callback = function(x as integer)\n return x\n end function'); + }); + + it('types an arrow function class field as Function in typedefs', async () => { + await testGetTypedef(` + class Person + callback = (x as integer) => x + end class + `, ` + class Person + sub new() + end sub + public callback as Function + end class + `); + }); + + it('transpiles arrow functions inside conditional compile blocks', async () => { + await testTranspile(` + sub main() + #if true + f = (x) => x + #else + f = (x) => x * 2 + #end if + end sub + `, ` + sub main() + #if true + f = function(x) + return x + end function + #else + f = function(x) + return x * 2 + end function + #end if + end sub + `, 'trim', 'source/main.bs', false); + }); + + it('keeps a trailing comment', async () => { + await testTranspile(` + sub main() + twice = (x) => x * 2 ' double it + end sub + `, ` + sub main() + twice = function(x) + return x * 2 + end function ' double it + end sub + `); + }); + + it('becomes a sub when the body calls a built-in that returns nothing', async () => { + await testTranspile(` + sub main() + addItem = (list as integer[], item as integer) => list.push(item) + end sub + `, ` + sub main() + addItem = sub(list as dynamic, item as integer) + list.push(item) + end sub + end sub + `); + }); + + it('becomes a sub when the body calls a sub', async () => { + await testTranspile(` + sub main() + logIt = (message as string) => doLog(message) + end sub + sub doLog(message as string) + end sub + `, ` + sub main() + logIt = sub(message as string) + doLog(message) + end sub + end sub + + sub doLog(message as string) + end sub + `); + }); + + it('becomes a sub when the body calls a function declared as void', async () => { + await testTranspile(` + sub main() + logIt = (message as string) => doLog(message) + end sub + function doLog(message as string) as void + end function + `, ` + sub main() + logIt = sub(message as string) + doLog(message) + end sub + end sub + + function doLog(message as string) as void + end function + `); + }); + + it('becomes a sub when the body calls a namespaced sub', async () => { + await testTranspile(` + namespace alpha + sub doLog(message as string) + end sub + end namespace + sub main() + logIt = (message as string) => alpha.doLog(message) + end sub + `, ` + sub alpha_doLog(message as string) + end sub + + sub main() + logIt = sub(message as string) + alpha_doLog(message) + end sub + end sub + `); + }); + + it('becomes a sub when the return type is void', async () => { + await testTranspile(` + sub main() + logIt = (message as string) as void => doLog(message) + end sub + sub doLog(message as string) + end sub + `, ` + sub main() + logIt = sub(message as string) as void + doLog(message) + end sub + end sub + + sub doLog(message as string) + end sub + `); + }); + + it('stays a function when the body calls a function that returns a value', async () => { + await testTranspile(` + sub main() + upper = (text as string) => ucase(text) + twice = (x as integer) => doubleIt(x) + end sub + function doubleIt(x as integer) as integer + return x * 2 + end function + `, ` + sub main() + upper = function(text as string) + return ucase(text) + end function + twice = function(x as integer) + return doubleIt(x) + end function + end sub + + function doubleIt(x as integer) as integer + return x * 2 + end function + `); + }); + + it('stays a function when the body calls something of unknown type', async () => { + await testTranspile(` + sub main(callback) + run = (x) => callback(x) + end sub + `, ` + sub main(callback) + run = function(x) + return callback(x) + end function + end sub + `, 'trim', 'source/main.bs', false); + }); + }); + + describe('types', () => { + it('infers the return type from the body', () => { + const func = getArrowFunction(` + sub main() + twice = (x as integer) => x * 2 + print twice(2) + end sub + `); + expect(func.getType({ flags: SymbolTypeFlag.runtime }).toString()).to.eql('function (x as integer) as integer'); + }); + + it('infers string, boolean and class return types', () => { + program.setFile('source/main.bs', ` + class Person + end class + sub main() + concat = (a as string, b as string) => a + b + isBig = (a as integer) => a > 1 + make = () => new Person() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const file = program.getFile('source/main.bs'); + const arrows = file.ast.findChildren((node) => isFunctionExpression(node) && node.isArrowFunction, { walkMode: WalkMode.visitAllRecursive }); + const returnTypes = arrows.map(x => x.getType({ flags: SymbolTypeFlag.runtime }).returnType.toString()); + expect(returnTypes).to.eql(['string', 'boolean', 'Person']); + }); + + it('uses the declared return type when there is one', () => { + const func = getArrowFunction(` + sub main() + f = (x as integer) as dynamic => x + end sub + `); + expect(func.getType({ flags: SymbolTypeFlag.runtime }).toString()).to.eql('function (x as integer) as dynamic'); + }); + + it('treats untyped params as dynamic', () => { + const func = getArrowFunction(` + sub main() + f = (x) => x + end sub + `); + expect(func.getType({ flags: SymbolTypeFlag.runtime }).toString()).to.eql('function (x as dynamic) as dynamic'); + }); + + it('treats a body that calls a sub as void', () => { + const func = getArrowFunction(` + sub main() + logIt = (message as string) => doLog(message) + end sub + sub doLog(message as string) + end sub + `); + const type = func.getType({ flags: SymbolTypeFlag.runtime }); + expect(type.isSub).to.be.true; + expect(isVoidType(type.returnType)).to.be.true; + }); + + it('types a nested arrow function as a function returning a function', () => { + const func = getArrowFunction(` + sub main() + f = (a as integer) => (b as integer) => b + end sub + `); + //the inner arrow function is a plain function value, so this is a function that returns a function + expect(func.getType({ flags: SymbolTypeFlag.runtime }).toString()).to.eql('function (a as integer) as function (b as integer) as integer'); + }); + }); + + describe('validation', () => { + describe('captured variables', () => { + it('flags a captured local variable', () => { + program.setFile('source/main.bs', ` + sub main() + factor = 2 + scale = (x) => x * factor + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('factor') + ]); + }); + + it('flags a captured parameter', () => { + program.setFile('source/main.bs', ` + sub main(factor) + scale = (x) => x * factor + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('factor') + ]); + }); + + it('flags a captured variable that is called as a function', () => { + program.setFile('source/main.bs', ` + sub main() + callback = function(x) + return x + end function + f = (x) => callback(x) + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('callback') + ]); + }); + + it('flags a captured variable used through a member access', () => { + program.setFile('source/main.bs', ` + sub main() + config = { factor: 2 } + scale = (x) => x * config.factor + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('config') + ]); + }); + + it('flags a variable captured from an enclosing arrow function', () => { + program.setFile('source/main.bs', ` + sub main() + f = (a) => (b) => a + b + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('a') + ]); + }); + + it('flags a variable captured from an enclosing anonymous function', () => { + program.setFile('source/main.bs', ` + sub main() + g = function() + inner = 1 + f = (x) => x + inner + return f + end function + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('inner') + ]); + }); + + it('flags a local declared after the arrow function exactly once', () => { + program.setFile('source/main.bs', ` + sub main() + f = (x) => x + later + later = 1 + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('later') + ]); + }); + + it('flags a captured loop variable', () => { + program.setFile('source/main.bs', ` + sub main(items) + for each item in items + f = (x) => item + end for + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('item') + ]); + }); + + it('flags a captured exception variable', () => { + program.setFile('source/main.bs', ` + sub main() + try + print 1 + catch e + f = () => e + end try + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('e') + ]); + }); + + it('flags a captured variable used in a default param value', () => { + program.setFile('source/main.bs', ` + sub main() + factor = 2 + f = (x = factor) => x + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('factor') + ]); + }); + + it('flags each captured variable', () => { + program.setFile('source/main.bs', ` + sub main() + a = 1 + b = 2 + f = () => a + b + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotCaptureVariable('a'), + DiagnosticMessages.arrowFunctionCannotCaptureVariable('b') + ]); + }); + + it('still reports unknown names as cannot-find-name', () => { + program.setFile('source/main.bs', ` + sub main() + f = (x) => x + unknownThing + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.cannotFindName('unknownThing') + ]); + }); + + it('allows a param that shadows an enclosing local', () => { + program.setFile('source/main.bs', ` + sub main() + factor = 2 + f = (factor) => factor * 2 + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows a default value that refers to an earlier param', () => { + program.setFile('source/main.bs', ` + sub main() + f = (a, b = a) => a + b + print f(1) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows a regular anonymous function inside an arrow function to use the arrow params', () => { + program.setFile('source/main.bs', ` + sub main() + f = (x) => function(y) + return y + end function + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + }); + + describe('m', () => { + it('flags m', () => { + program.setFile('source/main.bs', ` + sub main() + render = (data) => m.view.setData(data) + getM = () => m + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotUseM(), + DiagnosticMessages.arrowFunctionCannotUseM() + ]); + }); + + it('flags m regardless of case', () => { + program.setFile('source/main.bs', ` + sub main() + getM = () => M + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotUseM() + ]); + }); + + it('flags m inside an arrow function in a class', () => { + program.setFile('source/main.bs', ` + class Person + name as string + callback = () => m.name + sub test() + f = () => m.name + end sub + end class + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotUseM(), + DiagnosticMessages.arrowFunctionCannotUseM() + ]); + }); + + it('flags m inside a nested arrow function', () => { + program.setFile('source/main.bs', ` + sub main() + f = (x) => (y) => m.value + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.arrowFunctionCannotUseM() + ]); + }); + + it('allows m inside a regular anonymous function nested in an arrow function', () => { + program.setFile('source/main.bs', ` + sub main() + f = (x) => function() + return m.value + end function + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows m in the enclosing function around an arrow function', () => { + program.setFile('source/main.bs', ` + sub main() + m.callback = (x) => x + print m.callback(1) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows a param named like a member of m to be passed in instead', () => { + program.setFile('source/main.bs', ` + sub main() + render = (data, view) => view.setData(data) + render({}, m.view) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + }); + + describe('allowed references', () => { + it('allows file-level functions, namespaces and globals', () => { + program.setFile('source/main.bs', ` + namespace alpha + function beta(x) + return x + end function + sub main() + f = (x) => beta(x) + g = (x) => alpha.beta(x) + h = (x) => lcase(x) + end sub + end namespace + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows functions from other files in the same scope', () => { + program.setFile('source/lib.bs', ` + function helper(x) + return x + end function + `); + program.setFile('source/main.bs', ` + sub main() + f = (x) => helper(x) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows enums, consts, classes and interfaces', () => { + program.setFile('source/main.bs', ` + enum Direction + up = 1 + end enum + const MAX = 10 + interface Movie + title as string + end interface + class Person + name as string + end class + sub main() + f = (x as integer) => x + Direction.up + MAX + g = (movie as Movie) => movie.title + h = () => new Person() + i = (p as Person) => p.name + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates member access on typed params', () => { + expect(getDiagnosticCodes(` + class Person + name as string + end class + sub main() + f = (p as Person) => p.age + end sub + `)).to.eql(['cannot-find-name']); + }); + }); + + describe('type checking', () => { + it('validates against function types', () => { + program.setFile('source/main.bs', ` + sub apply(callback as function(x as integer) as integer) + end sub + sub main() + apply((x as integer) => x * 2) + apply((x as string) => x + "!") + end sub + `); + program.validate(); + expect(program.getDiagnostics().map(x => x.code)).to.eql(['argument-type-mismatch']); + }); + + it('validates the param count against function types', () => { + expect(getDiagnosticCodes(` + sub apply(callback as function(x as integer) as integer) + end sub + sub main() + apply((a as integer, b as integer) => a + b) + end sub + `)).to.eql(['argument-type-mismatch']); + }); + + it('rejects a void arrow function where a value-returning function is expected', () => { + expect(getDiagnosticCodes(` + sub apply(callback as function(x as integer) as integer) + end sub + sub doLog(x) + end sub + sub main() + apply((x as integer) => doLog(x)) + end sub + `)).to.eql(['argument-type-mismatch']); + }); + + it('accepts a void arrow function where a void function is expected', () => { + program.setFile('source/main.bs', ` + sub applySub(callback as sub(x as integer) as void) + end sub + sub applyFunction(callback as function(x as integer) as void) + end sub + sub doLog(x) + end sub + sub main() + applySub((x as integer) => doLog(x)) + applyFunction((x as integer) => doLog(x)) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates calls to an arrow function', () => { + expect(getDiagnosticCodes(` + sub main() + twice = (x as integer) => x * 2 + print twice("a") + print twice() + print twice(1, 2) + end sub + `)).to.eql(['argument-type-mismatch', 'incorrect-argument-count', 'incorrect-argument-count']); + }); + + it('validates the arrow function type in a typed assignment', () => { + expect(getDiagnosticCodes(` + sub main() + good as function(x as integer) as integer = (x as integer) => x + bad as function(x as string) as string = (x as integer) => x + end sub + `)).to.eql(['assignment-type-mismatch']); + }); + + it('validates an arrow function returned from a function', () => { + expect(getDiagnosticCodes(` + function make() as function(x as integer) as integer + return (x as integer) => x + end function + function makeBad() as function(x as integer) as integer + return (x as string) => x + end function + `)).to.eql(['return-type-mismatch']); + }); + + it('flags a body that does not match the declared return type', () => { + expect(getDiagnosticCodes(` + sub main() + f = (x as integer) as string => x + end sub + `)).to.eql(['return-type-mismatch']); + }); + + it('flags a void return type with a body that produces a value', () => { + expect(getDiagnosticCodes(` + sub main() + f = (x as integer) as void => x + end sub + `)).to.eql(['return-type-mismatch']); + }); + + it('uses the inferred return type at call sites', () => { + expect(getDiagnosticCodes(` + sub main() + twice = (x as integer) => x * 2 + name as string = twice(1) + end sub + `)).to.eql(['assignment-type-mismatch']); + }); + + it('can be passed to untyped and plain function params', () => { + program.setFile('source/main.bs', ` + sub takesAnything(callback) + end sub + sub takesDynamic(callback as dynamic) + end sub + sub takesObject(callback as object) + end sub + sub takesFunction(callback as function) + end sub + sub main() + takesAnything((x) => x) + takesDynamic((x) => x) + takesObject((x) => x) + takesFunction((x) => x) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('can be stored and called through an AA', () => { + program.setFile('source/main.bs', ` + sub main() + handlers = { twice: (x as integer) => x * 2 } + print handlers.twice(2) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + }); + + describe('files', () => { + it('is not allowed in brs files', () => { + program.setFile('source/main.brs', ` + sub main() + f = (x) => x + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('arrow functions') + ]); + }); + + it('works in component scopes', () => { + program.setFile('components/Widget.xml', [ + '', + '', + '