Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/function-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Function Types

BrighterScript allows specific definitions of functions that are passed as parameters. This enables code with callbacks to fully type-checked, and for any return types of callbacks to be propagated through the code.

## Syntax

Function types support the same syntax as anonymous function definitions: parameter names, optional parameters, and parameter and return types.

```brighterscript
sub useCallBack(callback as function(name as string, num as integer) as string)
print "Result is: " + callback("hello", 7)
end sub
```

transpiles to

```BrightScript
sub useCallBack(callback as function)
print "Result is: " + callback("hello", 7)
end sub
```

## Use with Type Statements

Including full function signatures inside another function signature can be a bit verbose. Function types can be used in type statements to make a shorthand for easier reading.

```brighterscript
type TextChangeHandler = function(oldName as string, newName as string) as boolean

sub checkText(callback as TextChangeHandler)
if callback(m.oldName, m.newName)
print "Text Change OK"
end if
end sub
```

transpiles to

```BrightScript
sub checkText(callback as function)
if callback(m.oldName, m.newName)
print "Text Change OK"
end if
end sub
```

## Validation

Both the function type itself and the arguments when it is called and return values are validated.

Validating the function type:

```brighterscript
sub useCallback(callback as sub(input as string))
callback("hello")
end sub

sub testCallback()
' This is a validation error: "sub(input as integer)" is NOT compatible with "sub(input as string)"
useCallback(sub(input as integer)
print input + 1
end sub)
end sub
```

Validating the function call:

```brighterscript
sub useCallback(callback as sub(input as string))
' This is a validation error: "integer" is NOT compatible with "string"
callback(123)
end sub
```
8 changes: 8 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ catch ' look, no exception variable!
end try
```

## [Function Types](function-types.md)

```brighterscript
function useCallback(callback as function(input as string) as integer) as integer
return callback("test")
end function
```

## [Imports](imports.md)

```brighterscript
Expand Down
9 changes: 7 additions & 2 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,9 @@ export const defaultMaximumTruncationLength = 160;

export function typeCompatibilityMessage(actualTypeString: string, expectedTypeString: string, data: TypeCompatibilityData) {
let message = '';
if (!data) {
return message;
}
actualTypeString = data?.actualType?.toString() ?? actualTypeString;
expectedTypeString = data?.expectedType?.toString() ?? expectedTypeString;

Expand All @@ -1104,14 +1107,16 @@ export function typeCompatibilityMessage(actualTypeString: string, expectedTypeS
partBuilder: (x) => `\n member "${x.name}" should be '${x.expectedType}' but is '${x.actualType}'`,
maxLength: defaultMaximumTruncationLength
});
} else if (data?.actualParamCount !== data?.expectedParamCount) {
message = `. Type '${expectedTypeString}' requires ${data.expectedParamCount} parameter${data.expectedParamCount === 1 ? '' : 's'} but '${actualTypeString}' has ${data.actualParamCount}`;
} else if (data?.parameterMismatches?.length > 0) {
message = '. ' + util.truncate({
leadingText: `Type '${actualTypeString}' has incompatible parameters:`,
items: data.parameterMismatches,
itemSeparator: '',
partBuilder: (x) => {
let pExpected = x.data?.expectedType.toString() ?? 'dynamic';
let pActual = x.data?.actualType.toString() ?? 'dynamic';
let pExpected = x.data?.expectedType?.toString() ?? 'dynamic';
let pActual = x.data?.actualType?.toString() ?? 'dynamic';

if (x.expectedOptional !== x.actualOptional) {
pExpected += x.expectedOptional ? '?' : '';
Expand Down
155 changes: 154 additions & 1 deletion src/Scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { InterfaceType } from './types/InterfaceType';
import { ComponentType } from './types/ComponentType';
import { WalkMode, createVisitor } from './astUtils/visitors';
import type { BinaryExpression, CallExpression, DottedGetExpression, FunctionExpression } from './parser/Expression';
import { ObjectType, UninitializedType } from './types';
import { ObjectType, TypedFunctionType, UninitializedType } from './types';
import undent from 'undent';
import * as fsExtra from 'fs-extra';
import { InlineInterfaceType } from './types/InlineInterfaceType';
Expand Down Expand Up @@ -4598,6 +4598,159 @@ describe('Scope', () => {
expectTypeToBe(memberType, StringType);
});
});

describe('typed function expressions', () => {
it('correctly types parameters with typed function expressions', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as function(s as string) as integer)
value = callback("hello")
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, IntegerType);
});

it('correctly types parameters with typed function expressions with multiple parameters', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as function(s as string, i as integer) as integer)
value = callback("hello", 123)
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const callbackSymbol = table.getSymbol('callback', SymbolTypeFlag.runtime)[0];
expectTypeToBe(callbackSymbol.type, TypedFunctionType);
const callbackType = callbackSymbol.type as TypedFunctionType;
expect(callbackType.params.length).to.equal(2);
expectTypeToBe(callbackType.params[0].type, StringType);
expectTypeToBe(callbackType.params[1].type, IntegerType);
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, IntegerType);
});

it('can use type statement to define a typed function type', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
type MyCallback = function(s as string) as integer

sub testFunc(callback as MyCallback)
value = callback("hello")
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, IntegerType);
});

it('can use a union of typed function types', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
type CallbackA = function(s as string) as integer
type CallbackB = function(i as integer) as string

sub testFunc(input, callback as CallbackA or CallbackB)
value = callback(input)
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, UnionType);
const valueType = valueSymbol.type as UnionType;
expect(valueType.types).to.include(IntegerType.instance);
expect(valueType.types).to.include(StringType.instance);
});

it('function return types are greedy in the parser', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as function() as integer or string)
value = callback()
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, UnionType);
const valueType = valueSymbol.type as UnionType;
expect(valueType.types).to.include(IntegerType.instance);
expect(valueType.types).to.include(StringType.instance);
});

it('brackets in function types do not cause issues', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as function(s as string) as (integer or string))
value = callback("hello")
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, UnionType);
const valueType = valueSymbol.type as UnionType;
expect(valueType.types).to.include(IntegerType.instance);
expect(valueType.types).to.include(StringType.instance);
});

it('function types with unions in parameters work', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as function(s as string or integer) as integer)
value = callback("hello")
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const callbackSymbol = table.getSymbol('callback', SymbolTypeFlag.runtime)[0];
expectTypeToBe(callbackSymbol.type, TypedFunctionType);
const callbackType = callbackSymbol.type as TypedFunctionType;
expect(callbackType.params.length).to.equal(1);
expectTypeToBe(callbackType.params[0].type, UnionType);
const paramType = callbackType.params[0].type as UnionType;
expect(paramType.types).to.include(StringType.instance);
expect(paramType.types).to.include(IntegerType.instance);
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, IntegerType);
});

it('args can be union of function types', () => {
const file = program.setFile<BrsFile>('source/test.bs', `
sub testFunc(callback as (function(s as string) as string) or (function(s as string) as integer))
value = callback("hello")
print value
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const funcExprBody = file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitAllRecursive })[0].body;
const table = funcExprBody.getSymbolTable();
const valueSymbol = table.getSymbol('value', SymbolTypeFlag.runtime)[0];
expectTypeToBe(valueSymbol.type, UnionType);
const valueType = valueSymbol.type as UnionType;
expect(valueType.types).to.include(IntegerType.instance);
expect(valueType.types).to.include(StringType.instance);
});
});
});

describe('symbol tables with pocket tables', () => {
Expand Down
16 changes: 14 additions & 2 deletions src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Body, AssignmentStatement, Block, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement, TypecastStatement, AliasStatement, AugmentedAssignmentStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, ExitStatement, TypeStatement } from '../parser/Statement';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TernaryExpression, NullCoalescingExpression, PrintSeparatorExpression, TypecastExpression, TypedArrayExpression, TypeExpression, InlineInterfaceMemberExpression, InlineInterfaceExpression } from '../parser/Expression';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TernaryExpression, NullCoalescingExpression, PrintSeparatorExpression, TypecastExpression, TypedArrayExpression, TypeExpression, InlineInterfaceMemberExpression, InlineInterfaceExpression, TypedFunctionTypeExpression } from '../parser/Expression';
import type { BrsFile } from '../files/BrsFile';
import type { XmlFile } from '../files/XmlFile';
import type { BsDiagnostic, TypedefProvider } from '../interfaces';
Expand Down Expand Up @@ -333,6 +333,9 @@ export function isInlineInterfaceExpression(element: any): element is InlineInte
export function isInlineInterfaceMemberExpression(element: any): element is InlineInterfaceMemberExpression {
return element?.kind === AstNodeKind.InlineInterfaceMemberExpression;
}
export function isTypedFunctionTypeExpression(element: any): element is TypedFunctionTypeExpression {
return element?.kind === AstNodeKind.TypedFunctionTypeExpression;
}

// BscType reflection
export function isStringType(value: any): value is StringType {
Expand All @@ -349,6 +352,10 @@ export function isTypedFunctionType(value: any): value is TypedFunctionType {
return value?.kind === BscTypeKind.TypedFunctionType;
}

export function isTypedFunctionTypeLike(value: any): value is TypedFunctionType | TypeStatementType | UnionType {
return isTypedFunctionType(value) || isTypeStatementTypeOf(value, isTypedFunctionTypeLike) || isUnionTypeOf(value, isTypedFunctionTypeLike);
}

export function isFunctionType(value: any): value is FunctionType {
return value?.kind === BscTypeKind.FunctionType;
}
Expand Down Expand Up @@ -492,7 +499,12 @@ export function isCallFuncableType(target): target is CallFuncableType {
}

export function isCallableType(target): target is BaseFunctionType {
return isFunctionTypeLike(target) || isTypedFunctionType(target) || isObjectType(target) || (isDynamicType(target) && !isAnyReferenceType(target));
return isFunctionTypeLike(target) ||
isTypedFunctionTypeLike(target) ||
isTypeStatementTypeOf(target, isCallableType) ||
isUnionTypeOf(target, isCallableType) ||
isObjectType(target) ||
(isDynamicType(target) && !isAnyReferenceType(target));
}

export function isAnyReferenceType(target): target is AnyReferenceType {
Expand Down
Loading