From 7fee5a19a1cc791573066eb252b84ea657d97d2b Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 1 Nov 2024 08:40:09 -0400 Subject: [PATCH] Update docs with new `if else` ternary support --- docs/plugins.md | 32 +++++ docs/ternary-operator.md | 109 +++++++++++++----- scripts/compile-doc-examples.ts | 2 +- .../transpile/BrsFilePreTranspileProcessor.ts | 4 +- .../expression/TernaryExpression.spec.ts | 12 ++ 5 files changed, 127 insertions(+), 32 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index e851abe04..8122b0657 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -227,6 +227,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 afterScopeCreate?: (scope: Scope) => void; 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/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts index 9cc513c98..287f2d9ca 100644 --- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts @@ -1,5 +1,5 @@ import { createAssignmentStatement, createBlock, createDottedSetStatement, createIfStatement, createIndexedSetStatement, createToken } from '../../astUtils/creators'; -import { isAssignmentStatement, isBinaryExpression, isBlock, isBrsFile, isDottedGetExpression, isDottedSetStatement, isGroupingExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isUnaryExpression, isVariableExpression } from '../../astUtils/reflection'; +import { isAssignmentStatement, isBinaryExpression, isBlock, isBody, isBrsFile, isDottedGetExpression, isDottedSetStatement, isGroupingExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isUnaryExpression, isVariableExpression } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BrsFile } from '../../files/BrsFile'; import type { BeforeFileTranspileEvent } from '../../interfaces'; @@ -49,7 +49,7 @@ export class BrsFilePreTranspileProcessor { private processTernaryExpression(ternaryExpression: TernaryExpression, visitor: ReturnType, walkMode: WalkMode) { function getOwnerAndKey(statement: Statement) { const parent = statement.parent; - if (isBlock(parent)) { + if (isBlock(parent) || isBody(parent)) { let idx = parent.statements.indexOf(statement); if (idx > -1) { return { owner: parent.statements, key: idx }; diff --git a/src/parser/tests/expression/TernaryExpression.spec.ts b/src/parser/tests/expression/TernaryExpression.spec.ts index c0e39db6e..d1f5f0f3f 100644 --- a/src/parser/tests/expression/TernaryExpression.spec.ts +++ b/src/parser/tests/expression/TernaryExpression.spec.ts @@ -265,6 +265,18 @@ describe('ternary expressions', () => { program.dispose(); }); + it('transpiles top-level ternary expression', () => { + 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', () => { testTranspile(` sub main()