diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc6046c20..519cf029b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,8 +27,8 @@ jobs: - run: npm run lint - run: npm run test - run: npm run publish-coverage - # disable this task until we have v1-suported versions of the projects to test - # - run: npm run test-related-projects + # this is stalling out for some reason, so disable it for now + #- run: npm run test-related-projects npm-release: #only run this task if a tag starting with 'v' was used to trigger this (i.e. a tagged release) if: startsWith(github.ref, 'refs/tags/v') diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a54045c..ad59cf290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -358,6 +358,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.66.0-alpha.0](https://github.com/rokucommunity/brighterscript/compare/v0.65.1...v0.66.0-alpha.0) - 2023-06-09 ### Changed - all the type tracking stuff! + + + +## [0.68.0](https://github.com/rokucommunity/brighterscript/compare/v0.67.8...v0.68.0) - 2024-11-21 +### Changed + - Fix issues with the ast walkArray function ([#1347](https://github.com/rokucommunity/brighterscript/pull/1347)) + - Optimize ternary transpilation for assignments ([#1341](https://github.com/rokucommunity/brighterscript/pull/1341)) + + + ## [0.67.8](https://github.com/rokucommunity/brighterscript/compare/v0.67.7...v0.67.8) - 2024-10-18 ### Changed - upgrade to [roku-deploy@3.12.2](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3122---2024-10-18). Notable changes since 3.12.1: diff --git a/docs/plugins.md b/docs/plugins.md index e05098362..9a2e37ef3 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -273,6 +273,38 @@ export interface CompilerPlugin { afterProvideReferences?(event: AfterProvideReferencesEvent): any; + /** + * Called before the `provideDocumentSymbols` hook + */ + beforeProvideDocumentSymbols?(event: BeforeProvideDocumentSymbolsEvent): any; + /** + * Provide all of the `DocumentSymbol`s for the given file + * @param event + */ + provideDocumentSymbols?(event: ProvideDocumentSymbolsEvent): any; + /** + * Called after `provideDocumentSymbols`. Use this if you want to intercept or sanitize the document symbols data provided by bsc or other plugins + * @param event + */ + afterProvideDocumentSymbols?(event: AfterProvideDocumentSymbolsEvent): any; + + + /** + * Called before the `provideWorkspaceSymbols` hook + */ + beforeProvideWorkspaceSymbols?(event: BeforeProvideWorkspaceSymbolsEvent): any; + /** + * Provide all of the workspace symbols for the entire project + * @param event + */ + provideWorkspaceSymbols?(event: ProvideWorkspaceSymbolsEvent): any; + /** + * Called after `provideWorkspaceSymbols`. Use this if you want to intercept or sanitize the workspace symbols data provided by bsc or other plugins + * @param event + */ + afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any; + + onGetSemanticTokens?: PluginHandler; //scope events onScopeValidate?(event: OnScopeValidateEvent): any; diff --git a/docs/ternary-operator.md b/docs/ternary-operator.md index 0d8db0364..a929ba59c 100644 --- a/docs/ternary-operator.md +++ b/docs/ternary-operator.md @@ -1,20 +1,61 @@ # Ternary (Conditional) Operator: ? -The ternary (conditional) operator is the only BrighterScript operator that takes three operands: a condition followed by a question mark (?), then an expression to execute (consequent) if the condition is true followed by a colon (:), and finally the expression to execute (alternate) if the condition is false. This operator is frequently used as a shortcut for the if statement. It can be used in assignments, and in any other place where an expression is valid. Due to ambiguity in the brightscript syntax, ternary operators cannot be used as standalone statements. See the [No standalone statements](#no-standalone-statements) section for more information. +The ternary (conditional) operator takes three operands: a condition followed by a question mark (?), then an expression to execute (consequent) if the condition is true followed by a colon (:), and finally the expression to execute (alternate) if the condition is false. This operator is frequently used as a shorthand version of an if statement. It can be used in assignments or any place where an expression is valid. Due to ambiguity in the brightscript syntax, ternary operators cannot be used as standalone statements. See the [No standalone statements](#no-standalone-statements) section for more information. ## Warning

The optional chaining operator was added to the BrightScript runtime in Roku OS 11, which introduced a slight limitation to the BrighterScript ternary operator. As such, all ternary expressions must have a space to the right of the question mark when followed by [ or (. See the optional chaning section for more information.

## Basic usage +The basic syntax is: ` ? : `. +Here's an example: ```BrighterScript -authStatus = user <> invalid ? "logged in" : "not logged in" +authStatus = user2 <> invalid ? "logged in" : "not logged in" ``` transpiles to: ```BrightScript -authStatus = bslib_ternary(user <> invalid, "logged in", "not logged in") +if user2 <> invalid then + authStatus = "logged in" +else + authStatus = "not logged in" +end if +``` + +As you can see, when used in the right-hand-side of an assignment, brighterscript transpiles the ternary expression into a simple `if else` block. +The `bslib_ternary` function checks the condition, and returns either the consequent or alternate. In these optimal situations, brighterscript can generate the most efficient code possible which is equivalent to what you'd write yourself without access to the ternary expression. + +This also works for nested ternary expressions: +```BrighterScript +result = true ? (true ? "one" : "two") : "three" +``` + +transpiles to: + +```BrightScript +if true then + if true then + result = "one" + else + result = "two" + end if +else + result = "three" +end if +``` + +## Use in complex expressions +Ternary can also be used in any location where expressions are supported, such as function calls or within array or associative array declarations. In some situations it's much more difficult to convert the ternary expression into an `if else` statement, so we need to leverage the brighterscript `bslib_ternary` library function instead. Consider the following example: + +```BrighterScript + printAuthStatus(user2 <> invalid ? "logged in" : "not logged in") +``` + +transpiles to: + +```BrightScript +printAuthStatus(bslib_ternary(user2 <> invalid, "logged in", "not logged in")) ``` The `bslib_ternary` function checks the condition, and returns either the consequent or alternate. @@ -24,86 +65,94 @@ There are some important implications to consider for code execution order and s Consider: ```BrighterScript - a = user = invalid ? "no name" : user.name + printUsername(user = invalid ? "no name" : user.name) ``` -transpiles to: +
+ View the transpiled BrightScript code + ```BrightScript -a = (function(__bsCondition, user) +printUsername((function(__bsCondition, user) if __bsCondition then return "no name" else return user.name end if - end function)(user = invalid, user) + end function)(user = invalid, user)) ``` +
-This code will crash because `user.invalid` will be evaluated. -To avoid this problem the transpiler provides conditional scope protection, as discussed in the following section. +If we were to transpile this to leverage the `bslib_ternary` function, it would crash at runtime because `user.name` will be evaluated even when `user` is `invalid`. To avoid this problem the transpiler provides conditional scope protection, as discussed in the following section. ## Scope protection -For conditional language features such as the _conditional (ternary) operator_, BrighterScript sometimes needs to protect against unintended performance hits. - -There are 2 possible ways that your code can be transpiled: +Sometimes BrighterScript needs to protect against unintended performance hits. There are 3 possible ways that your code can be transpiled: -### Simple -In this situation, BrighterScript has determined that both the consequent and the alternate are side-effect free and will not cause rendezvous. This means BrighterScript can use a simpler and more performant transpile target. +### Optimized `if else` block +In this situation, brighterscript can safely convert the ternary expression into an `if else` block. This is typically available when ternary is used in assignments. ```BrighterScript -a = user = invalid ? "not logged in" : "logged in" +m.username = m.user <> invalid ? m.user.name : invalid ``` transpiles to: ```BrightScript -a = bslib_ternary(user = invalid, "not logged in", "logged in") +if m.user <> invalid then + m.username = m.user.name +else + m.username = invalid +end if ``` +### BrighterScript `bslib_ternary` library function +In this situation, BrighterScript has determined that both the consequent and the alternate are side-effect free and will not cause rendezvous or cloning, but the code was too complex to safely transpile to an `if else` block so we will leverage the `bslib_ternary` function instead. + ```BrighterScript -a = user = invalid ? defaultUser : user +printAuthStatus(user = invalid ? "not logged in" : "logged in") ``` transpiles to: ```BrightScript -a = bslib_ternary(user = invalid, defaultUser, user) +printAuthStatus(bslib_ternary(user = invalid, "not logged in", "logged in")) ``` + ### Scope capturing -In this situation, BrighterScript has detected that your ternary operation will have side-effects or could possibly result in a rendezvous. BrighterScript will create an immediately-invoked-function-expression to capture all of the referenced local variables. This is in order to only execute the consequent if the condition is true, and only execute the alternate if the condition is false. +In this situation, BrighterScript has detected that your ternary operation will have side-effects or could possibly result in a rendezvous, and could not be transpiled to an `if else` block. BrighterScript will create an [immediately-invoked-function-expression](https://en.wikipedia.org/wiki/Immediately_invoked_function_expression) to capture all of the referenced local variables. This is in order to only execute the consequent if the condition is true, and only execute the alternate if the condition is false. ```BrighterScript - a = user = invalid ? "no name" : user.name + printUsername(user = invalid ? "no name" : user.name) ``` transpiles to: ```BrightScript -a = (function(__bsCondition, user) +printUsername((function(__bsCondition, user) if __bsCondition then return "no name" else return user.name end if - end function)(user = invalid, user) + end function)(user = invalid, user)) ``` ```BrighterScript -a = user = invalid ? getNoNameMessage(m.config) : user.name + m.accountType +printMessage(user = invalid ? getNoNameMessage(m.config) : user.name + m.accountType) ``` transpiles to: ```BrightScript -a = (function(__bsCondition, getNoNameMessage, m, user) +printMessage((function(__bsCondition, getNoNameMessage, m, user) if __bsCondition then return getNoNameMessage(m.config) else return user.name + m.accountType end if - end function)(user = invalid, getNoNameMessage, m, user) + end function)(user = invalid, getNoNameMessage, m, user)) ``` ### Nested Scope Protection @@ -114,17 +163,18 @@ m.increment = function () m.count++ return m.count end function -result = (m.increment() = 1 ? m.increment() : -1) = -1 ? m.increment(): -1 +printResult((m.increment() = 1 ? m.increment() : -1) = -1 ? m.increment(): -1) ``` transpiles to: + ```BrightScript m.count = 1 m.increment = function() m.count++ return m.count end function -result = (function(__bsCondition, m) +printResult((function(__bsCondition, m) if __bsCondition then return m.increment() else @@ -136,7 +186,7 @@ result = (function(__bsCondition, m) else return -1 end if - end function)(m.increment() = 1, m)) = -1, m) + end function)(m.increment() = 1, m)) = -1, m)) ``` @@ -155,7 +205,7 @@ end function ``` ## No standalone statements -The ternary operator may only be used in expressions and may not be used in standalone statements because the BrightScript grammer uses `=` for both assignments (`a = b`) and conditions (`if a = b`) +The ternary operator may only be used in expressions, and may not be used in standalone statements because the BrightScript grammer uses `=` for both assignments (`a = b`) and conditions (`if a = b`) ```brightscript ' this is generally not valid @@ -173,6 +223,7 @@ This expression can be interpreted in two completely separate ways: ```brightscript 'assignment a = (myValue ? "a" : "b'") + 'ternary (a = myValue) ? "a" : "b" ``` diff --git a/scripts/compile-doc-examples.ts b/scripts/compile-doc-examples.ts index 5ec846721..3e50bc28a 100644 --- a/scripts/compile-doc-examples.ts +++ b/scripts/compile-doc-examples.ts @@ -197,7 +197,7 @@ class DocCompiler { const program = new Program({ rootDir: `${__dirname}/rootDir`, files: [ - 'source/main.brs' + 'source/main.bs' ], //use the current bsconfig ...(this.bsconfig ?? {}) diff --git a/src/astUtils/creators.ts b/src/astUtils/creators.ts index 0dd8a241c..8fda2d661 100644 --- a/src/astUtils/creators.ts +++ b/src/astUtils/creators.ts @@ -3,9 +3,9 @@ import type { Identifier, Token } from '../lexer/Token'; import type { SGToken } from '../parser/SGTypes'; import { SGAttribute, SGComponent, SGInterface, SGInterfaceField, SGInterfaceFunction, SGScript } from '../parser/SGTypes'; import { TokenKind } from '../lexer/TokenKind'; -import type { Expression } from '../parser/AstNode'; -import { CallExpression, DottedGetExpression, FunctionExpression, LiteralExpression, VariableExpression } from '../parser/Expression'; -import { Block, MethodStatement } from '../parser/Statement'; +import type { Expression, Statement } from '../parser/AstNode'; +import { LiteralExpression, CallExpression, DottedGetExpression, VariableExpression, FunctionExpression } from '../parser/Expression'; +import { AssignmentStatement, Block, DottedSetStatement, IfStatement, IndexedSetStatement, MethodStatement } from '../parser/Statement'; const tokenDefaults = { [TokenKind.BackTick]: '`', @@ -263,3 +263,75 @@ export function createSGScript(attributes: { type?: string; uri?: string }) { startTagClose: { text: '/>' } }); } + +export function createIfStatement(options: { + if?: Token; + condition: Expression; + then?: Token; + thenBranch: Block; + else?: Token; + elseBranch?: IfStatement | Block; + endIf?: Token; +}) { + return new IfStatement( + { + if: options.if ?? createToken(TokenKind.If), + condition: options.condition, + then: options.then ?? createToken(TokenKind.Then), + thenBranch: options.thenBranch, + else: options.else ?? createToken(TokenKind.Else), + elseBranch: options.elseBranch, + endIf: options.endIf ?? createToken(TokenKind.EndIf) + } + ); +} + +export function createBlock(options: { statements: Statement[] }) { + return new Block(options); +} + +export function createAssignmentStatement(options: { + name: Identifier | string; + equals?: Token; + value: Expression; +}) { + return new AssignmentStatement({ + equals: options.equals ?? createToken(TokenKind.Equal), + name: typeof options.name === 'string' ? createIdentifier(options.name) : options.name, + value: options.value + }); +} + +export function createDottedSetStatement(options: { + obj: Expression; + dot?: Token; + name: Identifier | string; + equals?: Token; + value: Expression; +}) { + return new DottedSetStatement({ + obj: options.obj, + name: typeof options.name === 'string' ? createIdentifier(options.name) : options.name, + value: options.value, + dot: options.dot, + equals: options.equals ?? createToken(TokenKind.Equal) + }); +} + +export function createIndexedSetStatement(options: { + obj: Expression; + openingSquare?: Token; + indexes: Expression[]; + closingSquare?: Token; + equals?: Token; + value: Expression; +}) { + return new IndexedSetStatement({ + obj: options.obj, + indexes: options.indexes, + value: options.value, + openingSquare: options.openingSquare ?? createToken(TokenKind.LeftSquareBracket), + closingSquare: options.closingSquare ?? createToken(TokenKind.RightSquareBracket), + equals: options.equals ?? createToken(TokenKind.Equal) + }); +} diff --git a/src/astUtils/visitors.spec.ts b/src/astUtils/visitors.spec.ts index 126e329ec..049a88689 100644 --- a/src/astUtils/visitors.spec.ts +++ b/src/astUtils/visitors.spec.ts @@ -8,15 +8,17 @@ import type { BrsFile } from '../files/BrsFile'; import type { FunctionStatement } from '../parser/Statement'; import { PrintStatement, Block, ReturnStatement, ExpressionStatement } from '../parser/Statement'; import { TokenKind } from '../lexer/TokenKind'; -import { ChildrenSkipper, createVisitor, InternalWalkMode, WalkMode, walkStatements } from './visitors'; -import { isFunctionExpression, isPrintStatement } from './reflection'; -import { createCall, createToken, createVariableExpression } from './creators'; +import { ChildrenSkipper, createVisitor, InternalWalkMode, walkArray, WalkMode, walkStatements } from './visitors'; +import { isBlock, isFunctionExpression, isLiteralExpression, isPrintStatement } from './reflection'; +import { createCall, createIntegerLiteral, createToken, createVariableExpression } from './creators'; import { createStackedVisitor } from './stackedVisitor'; import { Editor } from './Editor'; import { ParseMode, Parser } from '../parser/Parser'; import type { Statement, Expression, AstNode } from '../parser/AstNode'; import { expectZeroDiagnostics } from '../testHelpers.spec'; -import type { FunctionExpression } from '../parser/Expression'; +import type { VariableExpression } from '../parser/Expression'; +import { BinaryExpression, type FunctionExpression, type LiteralExpression } from '../parser/Expression'; + describe('astUtils visitors', () => { const rootDir = process.cwd(); let program: Program; @@ -1108,6 +1110,88 @@ describe('astUtils visitors', () => { ).to.be.lengthOf(0); }); + it('walks everything when the first element is replaced', () => { + const { ast } = Parser.parse(` + sub main() + print 1 + print 2 + print 3 + end sub + `); + const target = ast.findChild(isBlock).statements[0]; + let callCount = 0; + ast.walk((astNode, parent, owner: Statement[], key) => { + if (isPrintStatement(astNode)) { + callCount++; + } + if (astNode === target) { + owner.splice(key, 1); + } + }, { + walkMode: WalkMode.visitAllRecursive + }); + //the visitor should have been called for every statement + expect(callCount).to.eql(3); + expect( + (ast.statements[0] as FunctionStatement).func.body.statements + ).not.to.include(target); + }); + + it('walks everything when the middle element is replaced', () => { + const { ast } = Parser.parse(` + sub main() + print 1 + print 2 + print 3 + end sub + `); + const target = ast.findChild(isBlock).statements[1]; + let callCount = 0; + ast.walk((astNode, parent, owner: Statement[], key) => { + if (isPrintStatement(astNode)) { + callCount++; + } + if (astNode === target) { + owner.splice(key, 1); + } + }, { + walkMode: WalkMode.visitAllRecursive + }); + //the visitor should have been called for every statement + expect(callCount).to.eql(3); + expect( + (ast.statements[0] as FunctionStatement).func.body.statements + ).not.to.include(target); + }); + + it('walks everything when the end element is replaced', () => { + const { ast } = Parser.parse(` + sub main() + print 1 + print 2 + print 3 + end sub + `); + const target = ast.findChild(isBlock).statements[2]; + let callCount = 0; + ast.walk((astNode, parent, owner: Statement[], key) => { + if (isPrintStatement(astNode)) { + callCount++; + } + if (astNode === target) { + owner.splice(key, 1); + } + }, { + walkMode: WalkMode.visitAllRecursive + }); + //the visitor should have been called for every statement + expect(callCount).to.eql(3); + expect( + (ast.statements[0] as FunctionStatement).func.body.statements + ).not.to.include(target); + }); + + it('can be used to insert statements', () => { const { ast } = Parser.parse(` sub main() @@ -1125,20 +1209,25 @@ describe('astUtils visitors', () => { //add another expression to the list every time. This should result in 1 the first time, 2 the second, 3 the third. calls.push(new ExpressionStatement({ expression: createCall( - createVariableExpression('doSomethingBeforePrint') + createVariableExpression('doSomethingBeforePrint'), + [ + createIntegerLiteral(callExpressionCount.toString()) + ] ) })); - owner.splice(key, 0, ...calls); + owner.splice(key + 1, 0, ...calls.map(x => x.clone())); }, - CallExpression: () => { + CallExpression: (call) => { callExpressionCount++; + console.log('call visitor for', (call.args[0] as LiteralExpression).tokens.value.text); } }), { walkMode: WalkMode.visitAllRecursive }); //the visitor should have been called for every statement expect(printStatementCount).to.eql(3); - expect(callExpressionCount).to.eql(0); + //since the calls were injected after each print statement, we should have 1 call for the first print, 2 for the second, and 3 for the third + expect(callExpressionCount).to.eql(6); expect( (ast.statements[0] as FunctionStatement).func.body.statements ).to.be.lengthOf(9); @@ -1248,8 +1337,34 @@ describe('astUtils visitors', () => { }), { walkMode: WalkMode.visitAllRecursive }); + }); + + it('walks a new child when returned from a visitor', () => { + let walkedLiterals: string[] = []; + + Parser.parse(` + sub main() + print 1 + 2 + end sub + `).ast.walk(createVisitor({ + BinaryExpression: (node, parent, owner: Statement[], key) => { + //replace the `1 + 2` binary expression with a new binary expression + if (isLiteralExpression(node.left) && node.left.tokens.value.text === '1') { + return new BinaryExpression({ + left: createIntegerLiteral('3'), + operator: createToken(TokenKind.Plus), + right: createIntegerLiteral('4') + }); + } + }, + LiteralExpression: (node) => { + walkedLiterals.push(node.tokens.value.text); + } + }), { + walkMode: WalkMode.visitAllRecursive + }); - expect(comments.length).to.eql(7); + expect(walkedLiterals).to.eql(['3', '4']); }); it('can set bsConst in walk', () => { @@ -1310,7 +1425,7 @@ describe('astUtils visitors', () => { expect(foundMainFunc).to.be.true; }); - it('will correct walk `not condition` cc blocks', () => { + it('will correctly walk `not condition` cc blocks', () => { const { ast } = program.setFile('source/main.brs', ` #const DEBUG = false #if not DEBUG @@ -1337,5 +1452,91 @@ describe('astUtils visitors', () => { expect(functionsFound.has('notDebug')).to.be.true; expect(functionsFound.has('notFalse')).to.be.true; }); + + it('walks a new child when returned from a visitor and using an AstEditor', () => { + let walkedLiterals: string[] = []; + + Parser.parse(` + sub main() + print 1 + 2 + end sub + `).ast.walk(createVisitor({ + BinaryExpression: (node, parent, owner: Statement[], key) => { + //replace the `1 + 2` binary expression with a new binary expression + if (isLiteralExpression(node.left) && node.left.tokens.value.text === '1') { + return new BinaryExpression({ + left: createIntegerLiteral('3'), + operator: createToken(TokenKind.Plus), + right: createIntegerLiteral('4') + }); + } + }, + LiteralExpression: (node) => { + walkedLiterals.push(node.tokens.value.text); + } + }), { + walkMode: WalkMode.visitAllRecursive, + editor: new Editor() + }); + + expect(walkedLiterals).to.eql(['3', '4']); + }); + }); + + describe('walkArray', () => { + const one = createVariableExpression('one'); + const two = createVariableExpression('two'); + const three = createVariableExpression('three'); + const four = createVariableExpression('four'); + const five = createVariableExpression('five'); + + function doTest(startingArray: VariableExpression[], expected: VariableExpression[], visitor?: (item: AstNode, parent: AstNode, owner: any, key: number) => any) { + const visitedItems: VariableExpression[] = []; + walkArray(startingArray, (item, parent, owner, key) => { + visitedItems.push(item as any); + return visitor?.(item, parent, owner, key); + }, { walkMode: WalkMode.visitAllRecursive }); + expect( + visitedItems.map(x => x.tokens.name.text) + ).to.eql( + expected.map(x => x.tokens.name.text) + ); + } + + it('walks every element in the array', () => { + doTest( + [one, two, three, four, five], + [one, two, three, four, five] + ); + }); + + it('walks new items added to the array', () => { + doTest( + [one, two], + [one, three, two, four], + (item, parent, owner, key) => { + //insert a value after one + if (item === one) { + owner.splice(key + 1, 0, three); + //insert a value after one + } else if (item === two) { + owner.splice(key + 1, 0, four); + } + } + ); + }); + + it('triggers on nodes that were skiped due to insertions', () => { + doTest( + [one, two, three], + [one, two, four, three], + (item, parent, owner, key) => { + //insert a value after one + if (item === two) { + owner.splice(key, 0, four); + } + } + ); + }); }); }); diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts index b1f43550e..01c4379e5 100644 --- a/src/astUtils/visitors.ts +++ b/src/astUtils/visitors.ts @@ -24,21 +24,25 @@ export type WalkVisitor = (node: AstNode, parent?: AstNode, owner?: /** * A helper function for Statement and Expression `walkAll` calls. + * @returns a new AstNode if it was changed by returning from the visitor, or undefined if not */ -export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: WalkOptions, parent?: AstNode) { +export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: WalkOptions, parent?: AstNode): AstNode | void { + let returnValue: AstNode | void; + //stop processing if canceled if (options.cancel?.isCancellationRequested) { - return; + return returnValue; } //the object we're visiting let element = owner[key] as any as AstNode; if (!element) { - return; + return returnValue; } //link this node to its parent - element.parent = parent ?? owner as unknown as AstNode; + parent = parent ?? owner as unknown as AstNode; + element.parent = parent; //get current bsConsts if (!options.bsConsts) { @@ -47,23 +51,24 @@ export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: W //notify the visitor of this element if (element.visitMode & options.walkMode) { - const result = visitor?.(element, element.parent as any, owner, key); + returnValue = visitor?.(element, element.parent as any, owner, key); //replace the value on the parent if the visitor returned a Statement or Expression (this is how visitors can edit AST) - if (result && (isExpression(result) || isStatement(result))) { + if (returnValue && (isExpression(returnValue) || isStatement(returnValue))) { + //if we have an editor, use that to modify the AST if (options.editor) { - options.editor.setProperty(owner, key, result as any); + options.editor.setProperty(owner, key, returnValue as any); + + //we don't have an editor, modify the AST directly } else { - (owner as any)[key] = result; - //don't walk the new element - return; + (owner as any)[key] = returnValue; } } } //stop processing if canceled if (options.cancel?.isCancellationRequested) { - return; + return returnValue; } //do not walk children if skipped @@ -72,11 +77,22 @@ export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: W return; } + //get the element again in case it was replaced by the visitor + element = owner[key] as any as AstNode; + if (!element) { + return returnValue; + } + + //set the parent of this new expression + element.parent = parent; + if (!element.walk) { throw new Error(`${owner.constructor.name}["${String(key)}"]${parent ? ` for ${parent.constructor.name}` : ''} does not contain a "walk" method`); } //walk the child expressions element.walk(visitor, options); + + return returnValue; } /** @@ -87,13 +103,28 @@ export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: W * @param parent the parent AstNode of each item in the array * @param filter a function used to filter items from the array. return true if that item should be walked */ -export function walkArray(array: Array, visitor: WalkVisitor, options: WalkOptions, parent?: AstNode, filter?: (element: T) => boolean) { +export function walkArray(array: Array, visitor: WalkVisitor, options: WalkOptions, parent?: AstNode, filter?: (element: T) => boolean) { + let processedNodes = new Set(); + for (let i = 0; i < array?.length; i++) { if (!filter || filter(array[i])) { - const startLength = array.length; - walk(array, i, visitor, options, parent); - //compensate for deleted or added items. - i += array.length - startLength; + let item = array[i]; + //skip already processed nodes for this array walk + if (processedNodes.has(item)) { + continue; + } + processedNodes.add(item); + + //if the walk produced a new node, we will assume the original node was handled, and the new node's children were walked, so we can skip it if we enter recovery mode + const newNode = walk(array, i, visitor, options, parent); + if (newNode) { + processedNodes.add(newNode); + } + + //if the current item changed, restart the entire loop (we'll skip any already-processed items) + if (array[i] !== item) { + i = -1; + } } } } diff --git a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts index ecabd62fe..d8fdda42a 100644 --- a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts @@ -1,14 +1,15 @@ -import { createToken } from '../../astUtils/creators'; +import { createAssignmentStatement, createBlock, createDottedSetStatement, createIfStatement, createIndexedSetStatement, createToken } from '../../astUtils/creators'; import type { Editor } from '../../astUtils/Editor'; -import { isDottedGetExpression, isLiteralExpression, isVariableExpression, isUnaryExpression, isAliasStatement, isCallExpression, isCallfuncExpression, isEnumType } from '../../astUtils/reflection'; +import { isDottedGetExpression, isLiteralExpression, isVariableExpression, isUnaryExpression, isAliasStatement, isCallExpression, isCallfuncExpression, isEnumType, isAssignmentStatement, isBlock, isBody, isDottedSetStatement, isGroupingExpression, isIndexedSetStatement, isAugmentedAssignmentStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BrsFile } from '../../files/BrsFile'; import type { ExtraSymbolData, OnPrepareFileEvent } from '../../interfaces'; import { TokenKind } from '../../lexer/TokenKind'; -import type { Expression } from '../../parser/AstNode'; +import type { Expression, Statement } from '../../parser/AstNode'; +import type { TernaryExpression } from '../../parser/Expression'; import { LiteralExpression, VariableExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; -import type { AliasStatement } from '../../parser/Statement'; +import { AugmentedAssignmentStatement, type AliasStatement, type IfStatement } from '../../parser/Statement'; import type { Scope } from '../../Scope'; import { SymbolTypeFlag } from '../../SymbolTypeFlag'; import util from '../../util'; @@ -40,6 +41,7 @@ export class BrsFilePreTranspileProcessor { private iterateExpressions() { const scope = this.event.program.getFirstScopeForFile(this.event.file); + //TODO move away from this loop and use a visitor instead // eslint-disable-next-line @typescript-eslint/dot-notation for (let expression of this.event.file['_cachedLookups'].expressions) { if (expression) { @@ -50,6 +52,153 @@ export class BrsFilePreTranspileProcessor { } } } + const walkMode = WalkMode.visitExpressionsRecursive; + const visitor = createVisitor({ + TernaryExpression: (ternaryExpression) => { + this.processTernaryExpression(ternaryExpression, visitor, walkMode); + } + }); + this.event.file.ast.walk(visitor, { walkMode: walkMode }); + } + + + private processTernaryExpression(ternaryExpression: TernaryExpression, visitor: ReturnType, walkMode: WalkMode) { + function getOwnerAndKey(statement: Statement) { + const parent = statement.parent; + if (isBlock(parent) || isBody(parent)) { + let idx = parent.statements.indexOf(statement); + if (idx > -1) { + return { owner: parent.statements, key: idx }; + } + } + } + + //if the ternary expression is part of a simple assignment, rewrite it as an `IfStatement` + let parent = ternaryExpression.findAncestor(x => !isGroupingExpression(x)); + let ifStatement: IfStatement; + + if (isAssignmentStatement(parent)) { + ifStatement = createIfStatement({ + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + condition: ternaryExpression.test, + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + thenBranch: createBlock({ + statements: [ + createAssignmentStatement({ + name: parent.tokens.name, + equals: parent.tokens.equals, + value: ternaryExpression.consequent + }) + ] + }), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + elseBranch: createBlock({ + statements: [ + createAssignmentStatement({ + name: util.cloneToken(parent.tokens.name), + equals: util.cloneToken(parent.tokens.equals), + value: ternaryExpression.alternate + }) + ] + }), + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + }); + } else if (isDottedSetStatement(parent)) { + ifStatement = createIfStatement({ + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + condition: ternaryExpression.test, + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + thenBranch: createBlock({ + statements: [ + createDottedSetStatement({ + obj: parent.obj, + name: parent.tokens.name, + equals: parent.tokens.equals, + value: ternaryExpression.consequent + }) + ] + }), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + elseBranch: createBlock({ + statements: [ + createDottedSetStatement({ + obj: parent.obj.clone(), + name: util.cloneToken(parent.tokens.name), + equals: util.cloneToken(parent.tokens.equals), + value: ternaryExpression.alternate + }) + ] + }), + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + }); + } else if (isIndexedSetStatement(parent)) { + ifStatement = createIfStatement({ + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + condition: ternaryExpression.test, + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + thenBranch: createBlock({ + statements: [ + createIndexedSetStatement({ + obj: parent.obj, + openingSquare: parent.tokens.openingSquare, + indexes: parent.indexes, + closingSquare: parent.tokens.closingSquare, + equals: parent.tokens.equals, + value: ternaryExpression.consequent + }) + ] + }), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + elseBranch: createBlock({ + statements: [ + createIndexedSetStatement({ + obj: parent.obj, + openingSquare: util.cloneToken(parent.tokens.openingSquare), + indexes: parent.indexes?.map(x => x.clone()), + closingSquare: util.cloneToken(parent.tokens.closingSquare), + equals: util.cloneToken(parent.tokens.equals), + value: ternaryExpression.alternate + }) + ] + }), + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + }); + } else if (isAugmentedAssignmentStatement(parent)) { + ifStatement = createIfStatement({ + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + condition: ternaryExpression.test, + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + thenBranch: createBlock({ + statements: [ + new AugmentedAssignmentStatement({ + item: parent.item, + operator: parent.tokens.operator, + value: ternaryExpression.consequent + }) + ] + }), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + elseBranch: createBlock({ + statements: [ + new AugmentedAssignmentStatement({ + item: parent.item.clone(), + operator: parent.tokens.operator, + value: ternaryExpression.alternate + }) + ] + }), + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + }); + } + + if (ifStatement) { + let { owner, key } = getOwnerAndKey(parent as Statement) ?? {}; + if (owner && key !== undefined) { + this.event.editor.setProperty(owner, key, ifStatement); + } + //we've injected an ifStatement, so now we need to trigger a walk to handle any nested ternary expressions + ifStatement.walk(visitor, { walkMode: walkMode }); + } } /** diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index f83e36765..28972fc64 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -836,6 +836,22 @@ describe('lexer', () => { expect(f.text).to.eql('2.5e3'); }); + it('supports very long numbers with !', () => { + function doTest(number: string) { + let f = Lexer.scan(number).tokens[0]; + expect(f.kind).to.equal(TokenKind.FloatLiteral); + expect(f.text).to.eql(number); + } + doTest('0!'); + doTest('0!'); + doTest('147483648!'); + doTest('2147483648!'); + doTest('2147483648111!'); + doTest('2.4e-38!'); + doTest('2.4e-32342342342342342342342342348!'); + doTest('2.4e+32342342342342342342342342348!'); + }); + it('supports larger-than-supported-precision floats to be defined with exponents', () => { let f = Lexer.scan('2.3659475627512424e-38').tokens[0]; expect(f.kind).to.equal(TokenKind.FloatLiteral); diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts index ad5f860a7..e3e21770e 100644 --- a/src/lexer/Lexer.ts +++ b/src/lexer/Lexer.ts @@ -7,6 +7,11 @@ import { DiagnosticMessages } from '../DiagnosticMessages'; import util from '../util'; import type { BsDiagnostic } from '../interfaces'; +/** + * Numeric type designators can only be one of these characters + */ +const numericTypeDesignatorCharsRegexp = /[#d!e&%]/; + export class Lexer { /** * The zero-indexed position at which the token under consideration begins. @@ -695,8 +700,12 @@ export class Lexer { let asString = this.source.slice(this.start, this.current); let numberOfDigits = containsDecimal ? asString.length - 1 : asString.length; let designator = this.peek().toLowerCase(); + //set to undefined if it's not one of the supported designator chars + if (!numericTypeDesignatorCharsRegexp.test(designator)) { + designator = undefined; + } - if (numberOfDigits >= 10 && designator !== '&' && designator !== 'e') { + if (numberOfDigits >= 10 && !designator) { // numeric literals over 10 digits with no type designator are implicitly Doubles this.addToken(TokenKind.DoubleLiteral); } else if (designator === '#') { @@ -727,9 +736,9 @@ export class Lexer { this.advance(); this.addToken(TokenKind.FloatLiteral); } else if (designator === 'e') { - // literals that use "E" as the exponent are also automatic Floats + // literals that use "e" as the exponent are also automatic Floats - // consume the "E" + // consume the "e" this.advance(); // exponents are optionally signed @@ -742,6 +751,11 @@ export class Lexer { this.advance(); } + //optionally consume a trailing type designator + if (numericTypeDesignatorCharsRegexp.test(this.peek())) { + this.advance(); + } + this.addToken(TokenKind.FloatLiteral); } else if (containsDecimal) { // anything with a decimal but without matching Double rules is a Float @@ -757,7 +771,6 @@ export class Lexer { } else { // otherwise, it's a regular integer this.addToken(TokenKind.IntegerLiteral); - } } diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index 15a5a996e..78c76acfb 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -192,7 +192,7 @@ export abstract class AstNode { } public getBsConsts() { - return this.bsConsts ?? this.parent?.getBsConsts(); + return this.bsConsts ?? this.parent?.getBsConsts?.(); } /** diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 2fcbf1b26..08beee917 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -15,9 +15,8 @@ import { createInvalidLiteral, createMethodStatement, createToken } from '../ast import { DynamicType } from '../types/DynamicType'; import type { BscType } from '../types/BscType'; import { SymbolTable } from '../SymbolTable'; -import type { Expression } from './AstNode'; -import { AstNodeKind } from './AstNode'; -import { Statement } from './AstNode'; +import type { AstNode, Expression } from './AstNode'; +import { AstNodeKind, Statement } from './AstNode'; import { ClassType } from '../types/ClassType'; import { EnumMemberType, EnumType } from '../types/EnumType'; import { NamespaceType } from '../types/NamespaceType'; @@ -946,7 +945,7 @@ export class PrintStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { if (options.walkMode & InternalWalkMode.walkExpressions) { //sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions - walkArray(this.expressions, visitor, options, this, (item) => isExpression(item as any)); + walkArray(this.expressions as AstNode[], visitor, options, this, (item) => isExpression(item as any)); } } @@ -1637,6 +1636,7 @@ export class DottedSetStatement extends Statement { this.location = util.createBoundingLocation( this.obj, this.tokens.dot, + this.tokens.equals, this.tokens.name, this.value ); @@ -1724,12 +1724,12 @@ export class IndexedSetStatement extends Statement { equals: options.equals }; this.obj = options.obj; - this.indexes = options.indexes; + this.indexes = options.indexes ?? []; this.value = options.value; this.location = util.createBoundingLocation( this.obj, this.tokens.openingSquare, - ...this.indexes ?? [], + ...this.indexes, this.tokens.closingSquare, this.value ); @@ -1795,9 +1795,9 @@ export class IndexedSetStatement extends Statement { obj: this.obj?.clone(), openingSquare: util.cloneToken(this.tokens.openingSquare), indexes: this.indexes?.map(x => x?.clone()), + closingSquare: util.cloneToken(this.tokens.closingSquare), equals: util.cloneToken(this.tokens.equals), - value: this.value?.clone(), - closingSquare: util.cloneToken(this.tokens.closingSquare) + value: this.value?.clone() }), ['obj', 'indexes', 'value'] ); diff --git a/src/parser/tests/expression/TernaryExpression.spec.ts b/src/parser/tests/expression/TernaryExpression.spec.ts index d468c026f..fc04df140 100644 --- a/src/parser/tests/expression/TernaryExpression.spec.ts +++ b/src/parser/tests/expression/TernaryExpression.spec.ts @@ -253,7 +253,7 @@ describe('ternary expressions', () => { }); }); - describe('transpilation', () => { + describe('transpile', () => { let rootDir = process.cwd(); let program: Program; let testTranspile = getTestTranspile(() => [program, rootDir]); @@ -265,17 +265,131 @@ describe('ternary expressions', () => { program.dispose(); }); + it('transpiles top-level ternary expression', async () => { + await testTranspile(` + a += true ? 1 : 2 + `, ` + if true then + a += 1 + else + a += 2 + end if + `, undefined, undefined, false); + }); + + it('transpiles ternary in RHS of AssignmentStatement to IfStatement', async () => { + await testTranspile(` + sub main() + a = true ? 1 : 2 + end sub + `, ` + sub main() + if true then + a = 1 + else + a = 2 + end if + end sub + `); + }); + + it('transpiles ternary in RHS of incrementor AssignmentStatement to IfStatement', async () => { + await testTranspile(` + sub main() + a = 1 + a += true ? 1 : 2 + end sub + `, ` + sub main() + a = 1 + if true then + a += 1 + else + a += 2 + end if + end sub + `); + }); + + it('transpiles ternary in RHS of DottedSetStatement to IfStatement', async () => { + await testTranspile(` + sub main() + m.a = true ? 1 : 2 + end sub + `, ` + sub main() + if true then + m.a = 1 + else + m.a = 2 + end if + end sub + `); + }); + + it('transpiles ternary in RHS of incrementor DottedSetStatement to IfStatement', async () => { + await testTranspile(` + sub main() + m.a += true ? 1 : 2 + end sub + `, ` + sub main() + if true then + m.a += 1 + else + m.a += 2 + end if + end sub + `); + }); + + it('transpiles ternary in RHS of IndexedSetStatement to IfStatement', async () => { + await testTranspile(` + sub main() + m["a"] = true ? 1 : 2 + end sub + `, ` + sub main() + if true then + m["a"] = 1 + else + m["a"] = 2 + end if + end sub + `); + }); + + it('transpiles ternary in RHS of incrementor IndexedSetStatement to IfStatement', async () => { + await testTranspile(` + sub main() + m["a"] += true ? 1 : 2 + end sub + `, ` + sub main() + if true then + m["a"] += 1 + else + m["a"] += 2 + end if + end sub + `); + }); + it('uses the proper prefix when aliased package is installed', async () => { program.setFile('source/roku_modules/rokucommunity_bslib/bslib.brs', ''); await testTranspile(` sub main() user = {} - a = user = invalid ? "no user" : "logged in" + result = [ + user = invalid ? "no user" : "logged in" + ] end sub `, ` sub main() user = {} - a = rokucommunity_bslib_ternary(user = invalid, "no user", "logged in") + result = [ + rokucommunity_bslib_ternary(user = invalid, "no user", "logged in") + ] end sub `); }); @@ -289,7 +403,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "no user", "logged in") + if user = invalid then + a = "no user" + else + a = "logged in" + end if end sub `); @@ -301,7 +419,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, 1, "logged in") + if user = invalid then + a = 1 + else + a = "logged in" + end if end sub `); @@ -313,7 +435,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, 1.2, "logged in") + if user = invalid then + a = 1.2 + else + a = "logged in" + end if end sub `); @@ -325,7 +451,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, {}, "logged in") + if user = invalid then + a = {} + else + a = "logged in" + end if end sub `); @@ -337,7 +467,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, [], "logged in") + if user = invalid then + a = [] + else + a = "logged in" + end if end sub `); }); @@ -351,7 +485,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "logged in", "no user") + if user = invalid then + a = "logged in" + else + a = "no user" + end if end sub `); @@ -363,7 +501,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "logged in", 1) + if user = invalid then + a = "logged in" + else + a = 1 + end if end sub `); @@ -375,7 +517,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "logged in", 1.2) + if user = invalid then + a = "logged in" + else + a = 1.2 + end if end sub `); @@ -387,7 +533,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "logged in", []) + if user = invalid then + a = "logged in" + else + a = [] + end if end sub `); @@ -399,7 +549,11 @@ describe('ternary expressions', () => { `, ` sub main() user = {} - a = bslib_ternary(user = invalid, "logged in", {}) + if user = invalid then + a = "logged in" + else + a = {} + end if end sub `); }); @@ -411,7 +565,11 @@ describe('ternary expressions', () => { end sub `, ` sub main() - a = bslib_ternary(str(123) = "123", true, false) + if str(123) = "123" then + a = true + else + a = false + end if end sub `); @@ -421,7 +579,11 @@ describe('ternary expressions', () => { end sub `, ` sub main() - a = bslib_ternary(m.top.service.IsTrue(), true, false) + if m.top.service.IsTrue() then + a = true + else + a = false + end if end sub `); @@ -437,7 +599,11 @@ describe('ternary expressions', () => { end sub sub main() - a = bslib_ternary(test(test(test(test(m.fifth()[123].truthy(1))))), true, false) + if test(test(test(test(m.fifth()[123].truthy(1))))) then + a = true + else + a = false + end if end sub `); }); @@ -446,18 +612,22 @@ describe('ternary expressions', () => { await testTranspile(` sub main() zombie = {} - name = zombie.getName() <> invalid ? zombie.GetName() : "zombie" + result = [ + zombie.getName() <> invalid ? zombie.GetName() : "zombie" + ] end sub `, ` sub main() zombie = {} - name = (function(__bsCondition, zombie) - if __bsCondition then - return zombie.GetName() - else - return "zombie" - end if - end function)(zombie.getName() <> invalid, zombie) + result = [ + (function(__bsCondition, zombie) + if __bsCondition then + return zombie.GetName() + else + return "zombie" + end if + end function)(zombie.getName() <> invalid, zombie) + ] end sub `); }); @@ -466,18 +636,22 @@ describe('ternary expressions', () => { await testTranspile(` sub main() zombie = {} - name = zombie.getName() = invalid ? "zombie" : zombie.GetName() + result = [ + zombie.getName() = invalid ? "zombie" : zombie.GetName() + ] end sub `, ` sub main() zombie = {} - name = (function(__bsCondition, zombie) - if __bsCondition then - return "zombie" - else - return zombie.GetName() - end if - end function)(zombie.getName() = invalid, zombie) + result = [ + (function(__bsCondition, zombie) + if __bsCondition then + return "zombie" + else + return zombie.GetName() + end if + end function)(zombie.getName() = invalid, zombie) + ] end sub `); }); @@ -486,23 +660,27 @@ describe('ternary expressions', () => { await testTranspile(` sub main() settings = {} - name = {} ? m.defaults.getAccount(settings.name) : "no" + result = [ + {} ? m.defaults.getAccount(settings.name) : "no" + ] end sub `, ` sub main() settings = {} - name = (function(__bsCondition, m, settings) - if __bsCondition then - return m.defaults.getAccount(settings.name) - else - return "no" - end if - end function)({}, m, settings) + result = [ + (function(__bsCondition, m, settings) + if __bsCondition then + return m.defaults.getAccount(settings.name) + else + return "no" + end if + end function)({}, m, settings) + ] end sub `); }); - it('ignores enum variable names', async () => { + it('ignores enum variable names for scope capturing', async () => { await testTranspile(` enum Direction up = "up" @@ -510,23 +688,27 @@ describe('ternary expressions', () => { end enum sub main() d = Direction.up - theDir = d = Direction.up ? Direction.up : false + result = [ + d = Direction.up ? Direction.up : false + ] end sub `, ` sub main() d = "up" - theDir = (function(__bsCondition) - if __bsCondition then - return "up" - else - return false - end if - end function)(d = "up") + result = [ + (function(__bsCondition) + if __bsCondition then + return "up" + else + return false + end if + end function)(d = "up") + ] end sub `); }); - it('ignores const variable names', async () => { + it('ignores const variable names for scope capturing', async () => { await testTranspile(` enum Direction up = "up" @@ -535,18 +717,22 @@ describe('ternary expressions', () => { const UP = "up" sub main() d = Direction.up - theDir = d = Direction.up ? UP : Direction.down + result = [ + d = Direction.up ? UP : Direction.down + ] end sub `, ` sub main() d = "up" - theDir = (function(__bsCondition) - if __bsCondition then - return "up" - else - return "down" - end if - end function)(d = "up") + result = [ + (function(__bsCondition) + if __bsCondition then + return "up" + else + return "down" + end if + end function)(d = "up") + ] end sub `); }); @@ -557,20 +743,116 @@ describe('ternary expressions', () => { sub main() zombie = {} human = {} - name = zombie <> invalid ? zombie.Attack(human <> invalid ? human: zombie) : "zombie" + result = zombie <> invalid ? zombie.Attack(human <> invalid ? human: zombie) : "zombie" end sub `, ` sub main() zombie = {} human = {} - name = (function(__bsCondition, human, zombie) - if __bsCondition then - return zombie.Attack(bslib_ternary(human <> invalid, human, zombie)) - else - return "zombie" - end if - end function)(zombie <> invalid, human, zombie) + if zombie <> invalid then + result = zombie.Attack(bslib_ternary(human <> invalid, human, zombie)) + else + result = "zombie" + end if + end sub + ` + ); + }); + + it('supports nested ternary in assignment', async () => { + await testTranspile( + ` + sub main() + result = true ? (false ? "one" : "two") : "three" + end sub + `, + ` + sub main() + if true then + if false then + result = "one" + else + result = "two" + end if + else + result = "three" + end if + end sub + ` + ); + }); + + it('supports nested ternary in DottedSet', async () => { + await testTranspile( + ` + sub main() + m.result = true ? (false ? "one" : "two") : "three" + end sub + `, + ` + sub main() + if true then + if false then + m.result = "one" + else + m.result = "two" + end if + else + m.result = "three" + end if + end sub + ` + ); + }); + + it('supports nested ternary in IndexedSet', async () => { + await testTranspile( + ` + sub main() + m["result"] = true ? (false ? "one" : "two") : "three" + end sub + `, + ` + sub main() + if true then + if false then + m["result"] = "one" + else + m["result"] = "two" + end if + else + m["result"] = "three" + end if + end sub + ` + ); + }); + + it('supports scope-captured outer, and simple inner', async () => { + await testTranspile( + ` + sub main() + zombie = {} + human = {} + result = [ + zombie <> invalid ? zombie.Attack(human <> invalid ? human: zombie) : "zombie" + ] + end sub + `, + ` + sub main() + zombie = {} + human = {} + result = [ + (function(__bsCondition, human, zombie) + if __bsCondition then + return zombie.Attack(bslib_ternary(human <> invalid, human, zombie)) + else + return "zombie" + end if + end function)(zombie <> invalid, human, zombie) + ] end sub ` ); @@ -581,19 +863,23 @@ describe('ternary expressions', () => { ` sub main() person = {} - name = person <> invalid ? person.name : "John Doe" + result = [ + person <> invalid ? person.name : "John Doe" + ] end sub `, ` sub main() person = {} - name = (function(__bsCondition, person) - if __bsCondition then - return person.name - else - return "John Doe" - end if - end function)(person <> invalid, person) + result = [ + (function(__bsCondition, person) + if __bsCondition then + return person.name + else + return "John Doe" + end if + end function)(person <> invalid, person) + ] end sub ` ); @@ -619,7 +905,6 @@ describe('ternary expressions', () => { `print bslib_ternary(name = "bob", invalid, invalid)` , 'none', undefined, false); }); - }); });