Repository: rokucommunity/brighterscript
Pull request #1341 (2024-10-31 19:57:39)

Optimize ternary transpilation for assignments
Whenever a ternary expression is on the right-hand-side of an assignment, dottedSet, or indexedSet statement, we can transpile the entire thing to an `if` statement instead which improves performance significantly (should be identical to handwritten non-ternary code). 

This PR adds this `if` statement transpiling for all of the following situations:
- assignments: `a = true ? 1 : 2`
- dotted set: `m.a = true ? 1 : 2`
- indexed set: `m["a"] = true ? 1 : 2`
- complex assignment operators (i.e. `+=`, `-=`, etc...) `size += true ? 1 : 2`
- grouped ternaries: `a = (((true ? 1 : 2)))`
- nested ternaries `a = true ? (false ? 1 : 2) : 3`

These are explicit cases, so any situation other than these will be transpiled according to the existing rules ([basic](https://github.com/rokucommunity/brighterscript/blob/master/docs/ternary-operator.md#basic-usage) and [scope safe](https://github.com/rokucommunity/brighterscript/blob/master/docs/ternary-operator.md#scope-protection))

**Simple example:**
```brighterscript
a = true ? 1 : 2
```

becomes this:
```brighterscript
if true then
    a = 1
else
    a = 2
end if
```

**Nested example:**
```brighterscript
result = true ? (false ? "one" : "two") : "three"
```

becomes this:
```brighterscript
if true then
    if false then
        result = "one"
    else
        result = "two"
    end if
else
    result = "three"
end if
```




Patch:
diff --git a/docs/plugins.md b/docs/plugins.md
index e851abe0..8122b065 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<OnGetSemanticTokensEvent>;
     //scope events
     afterScopeCreate?: (scope: Scope) => void;
diff --git a/docs/ternary-operator.md b/docs/ternary-operator.md
index 0d8db036..a929ba59 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
 <p style="background-color: #fdf8e3; color: #333; padding: 20px">The <a href="https://developer.roku.com/docs/references/brightscript/language/expressions-variables-types.md#optional-chaining-operators">optional chaining operator</a> was added to the BrightScript runtime in <a href="https://developer.roku.com/docs/developer-program/release-notes/roku-os-release-notes.md#roku-os-110">Roku OS 11</a>, 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 <b>[</b> or <b>(</b>. See the <a href="#">optional chaning</a> section for more information.
 </p>
 
 ## Basic usage
+The basic syntax is: `<condition> ? <value if true> : <value if false>`.
 
+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:
+<details>
+  <summary>View the transpiled BrightScript code</summary>
+
 ```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))
 ```
+</details>
 
-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 5ec84672..3e50bc28 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 2ca6964e..a36dc31b 100644
--- a/src/astUtils/creators.ts
+++ b/src/astUtils/creators.ts
@@ -1,10 +1,10 @@
 import type { Range } from 'vscode-languageserver';
 import type { Identifier, Token } from '../lexer/Token';
 import { TokenKind } from '../lexer/TokenKind';
-import type { Expression } from '../parser/AstNode';
+import type { Expression, Statement } from '../parser/AstNode';
 import { LiteralExpression, CallExpression, DottedGetExpression, VariableExpression, FunctionExpression } from '../parser/Expression';
 import type { SGAttribute } from '../parser/SGTypes';
-import { Block, MethodStatement } from '../parser/Statement';
+import { AssignmentStatement, Block, DottedSetStatement, IfStatement, IndexedSetStatement, MethodStatement } from '../parser/Statement';
 
 /**
  * A range that points to the beginning of the file. Used to give non-null ranges to programmatically-added source code.
@@ -187,3 +187,77 @@ export function createSGAttribute(keyName: string, value: string) {
         }
     } as SGAttribute;
 }
+
+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),
+            then: options.then ?? createToken(TokenKind.Then),
+            else: options.else ?? createToken(TokenKind.Else),
+            endIf: options.endIf ?? createToken(TokenKind.EndIf)
+        },
+        options.condition,
+        options.thenBranch,
+        options.elseBranch
+    );
+}
+
+export function createBlock(options: { statements: Statement[] }) {
+    return new Block(options.statements);
+}
+
+export function createAssignmentStatement(options: {
+    name: Identifier | string;
+    equals?: Token;
+    value: Expression;
+}) {
+    return new AssignmentStatement(
+        options.equals ?? createToken(TokenKind.Equal),
+        typeof options.name === 'string' ? createIdentifier(options.name) : options.name,
+        options.value
+    );
+}
+
+export function createDottedSetStatement(options: {
+    obj: Expression;
+    dot?: Token;
+    name: Identifier | string;
+    equals?: Token;
+    value: Expression;
+}) {
+    return new DottedSetStatement(
+        options.obj,
+        typeof options.name === 'string' ? createIdentifier(options.name) : options.name,
+        options.value,
+        options.dot,
+        options.equals ?? createToken(TokenKind.Equal)
+    );
+}
+
+export function createIndexedSetStatement(options: {
+    obj: Expression;
+    openingSquare?: Token;
+    index: Expression;
+    closingSquare?: Token;
+    equals?: Token;
+    value: Expression;
+    additionalIndexes?: Expression[];
+}) {
+    return new IndexedSetStatement(
+        options.obj,
+        options.index,
+        options.value,
+        options.openingSquare ?? createToken(TokenKind.LeftSquareBracket),
+        options.closingSquare ?? createToken(TokenKind.RightSquareBracket),
+        options.additionalIndexes || [],
+        options.equals ?? createToken(TokenKind.Equal)
+    );
+}
diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
index 965f5402..287f2d9c 100644
--- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
+++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
@@ -1,11 +1,15 @@
-import { createToken } from '../../astUtils/creators';
-import { isBrsFile, isDottedGetExpression, isLiteralExpression, isUnaryExpression, isVariableExpression } from '../../astUtils/reflection';
+import { createAssignmentStatement, createBlock, createDottedSetStatement, createIfStatement, createIndexedSetStatement, createToken } from '../../astUtils/creators';
+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';
+import type { Token } from '../../lexer/Token';
 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 } from '../../parser/Expression';
 import { ParseMode } from '../../parser/Parser';
+import type { IfStatement } from '../../parser/Statement';
 import type { Scope } from '../../Scope';
 import util from '../../util';
 
@@ -23,6 +27,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
         for (let expression of this.event.file.parser.references.expressions) {
             if (expression) {
                 if (isUnaryExpression(expression)) {
@@ -32,6 +37,144 @@ 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<typeof createVisitor>, 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 operator: Token;
+        //operators like `+=` will cause the RHS to be a BinaryExpression  due to how the parser handles this. let's do a little magic to detect this situation
+        if (
+            //parent is a binary expression
+            isBinaryExpression(parent) &&
+            (
+                (isAssignmentStatement(parent.parent) && isVariableExpression(parent.left) && parent.left.name === parent.parent.name) ||
+                (isDottedSetStatement(parent.parent) && isDottedGetExpression(parent.left) && parent.left.name === parent.parent.name) ||
+                (isIndexedSetStatement(parent.parent) && isIndexedGetExpression(parent.left) && parent.left.index === parent.parent.index)
+            )
+        ) {
+            //keep the correct operator (i.e. `+=`)
+            operator = parent.operator;
+            //use the outer parent and skip this BinaryExpression
+            parent = parent.parent;
+        }
+        let ifStatement: IfStatement;
+
+        if (isAssignmentStatement(parent)) {
+            ifStatement = createIfStatement({
+                if: createToken(TokenKind.If, 'if', ternaryExpression.questionMarkToken.range),
+                condition: ternaryExpression.test,
+                then: createToken(TokenKind.Then, 'then', ternaryExpression.questionMarkToken.range),
+                thenBranch: createBlock({
+                    statements: [
+                        createAssignmentStatement({
+                            name: parent.name,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.consequent
+                        })
+                    ]
+                }),
+                else: createToken(TokenKind.Else, 'else', ternaryExpression.questionMarkToken.range),
+                elseBranch: createBlock({
+                    statements: [
+                        createAssignmentStatement({
+                            name: parent.name,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.alternate
+                        })
+                    ]
+                }),
+                endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.questionMarkToken.range)
+            });
+        } else if (isDottedSetStatement(parent)) {
+            ifStatement = createIfStatement({
+                if: createToken(TokenKind.If, 'if', ternaryExpression.questionMarkToken.range),
+                condition: ternaryExpression.test,
+                then: createToken(TokenKind.Then, 'then', ternaryExpression.questionMarkToken.range),
+                thenBranch: createBlock({
+                    statements: [
+                        createDottedSetStatement({
+                            obj: parent.obj,
+                            name: parent.name,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.consequent
+                        })
+                    ]
+                }),
+                else: createToken(TokenKind.Else, 'else', ternaryExpression.questionMarkToken.range),
+                elseBranch: createBlock({
+                    statements: [
+                        createDottedSetStatement({
+                            obj: parent.obj,
+                            name: parent.name,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.alternate
+                        })
+                    ]
+                }),
+                endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.questionMarkToken.range)
+            });
+        } else if (isIndexedSetStatement(parent)) {
+            ifStatement = createIfStatement({
+                if: createToken(TokenKind.If, 'if', ternaryExpression.questionMarkToken.range),
+                condition: ternaryExpression.test,
+                then: createToken(TokenKind.Then, 'then', ternaryExpression.questionMarkToken.range),
+                thenBranch: createBlock({
+                    statements: [
+                        createIndexedSetStatement({
+                            obj: parent.obj,
+                            openingSquare: parent.openingSquare,
+                            index: parent.index,
+                            closingSquare: parent.closingSquare,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.consequent,
+                            additionalIndexes: parent.additionalIndexes
+                        })
+                    ]
+                }),
+                else: createToken(TokenKind.Else, 'else', ternaryExpression.questionMarkToken.range),
+                elseBranch: createBlock({
+                    statements: [
+                        createIndexedSetStatement({
+                            obj: parent.obj,
+                            openingSquare: parent.openingSquare,
+                            index: parent.index,
+                            closingSquare: parent.closingSquare,
+                            equals: operator ?? parent.equals,
+                            value: ternaryExpression.alternate,
+                            additionalIndexes: parent.additionalIndexes
+                        })
+                    ]
+                }),
+                endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.questionMarkToken.range)
+            });
+        }
+
+        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 });
+        }
     }
 
     /**
@@ -65,10 +208,10 @@ export class BrsFilePreTranspileProcessor {
 
     }
 
-    private processExpression(expression: Expression, scope: Scope | undefined) {
-        let containingNamespace = this.event.file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
+    private processExpression(ternaryExpression: Expression, scope: Scope | undefined) {
+        let containingNamespace = this.event.file.getNamespaceStatementForPosition(ternaryExpression.range.start)?.getName(ParseMode.BrighterScript);
 
-        const parts = util.splitExpression(expression);
+        const parts = util.splitExpression(ternaryExpression);
 
         const processedNames: string[] = [];
         for (let part of parts) {
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 5069c6c4..a824cb83 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -1055,7 +1055,7 @@ export class Parser {
         } else {
             const nameExpression = new VariableExpression(name);
             result = new AssignmentStatement(
-                operator,
+                { kind: TokenKind.Equal, text: '=', range: operator.range },
                 name,
                 new BinaryExpression(nameExpression, operator, value)
             );
@@ -2090,7 +2090,10 @@ export class Parser {
                         : new BinaryExpression(left, operator, right),
                     left.openingSquare,
                     left.closingSquare,
-                    left.additionalIndexes
+                    left.additionalIndexes,
+                    operator.kind === TokenKind.Equal
+                        ? operator
+                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
                 );
             } else if (isDottedGetExpression(left)) {
                 return new DottedSetStatement(
@@ -2099,7 +2102,10 @@ export class Parser {
                     operator.kind === TokenKind.Equal
                         ? right
                         : new BinaryExpression(left, operator, right),
-                    left.dot
+                    left.dot,
+                    operator.kind === TokenKind.Equal
+                        ? operator
+                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
                 );
             }
         }
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 9cd321c7..6af05737 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -1229,13 +1229,15 @@ export class DottedSetStatement extends Statement {
         readonly obj: Expression,
         readonly name: Identifier,
         readonly value: Expression,
-        readonly dot?: Token
+        readonly dot?: Token,
+        readonly equals?: Token
     ) {
         super();
         this.range = util.createBoundingRange(
             obj,
             dot,
             name,
+            equals,
             value
         );
     }
@@ -1253,7 +1255,9 @@ export class DottedSetStatement extends Statement {
                 this.dot ? state.tokenToSourceNode(this.dot) : '.',
                 //name
                 state.transpileToken(this.name),
-                ' = ',
+                ' ',
+                state.transpileToken(this.equals, '='),
+                ' ',
                 //right-hand-side of assignment
                 ...this.value.transpile(state)
             ];
@@ -1273,7 +1277,8 @@ export class DottedSetStatement extends Statement {
                 this.obj?.clone(),
                 util.cloneToken(this.name),
                 this.value?.clone(),
-                util.cloneToken(this.dot)
+                util.cloneToken(this.dot),
+                util.cloneToken(this.equals)
             ),
             ['obj', 'value']
         );
@@ -1287,17 +1292,20 @@ export class IndexedSetStatement extends Statement {
         readonly value: Expression,
         readonly openingSquare: Token,
         readonly closingSquare: Token,
-        readonly additionalIndexes?: Expression[]
+        readonly additionalIndexes?: Expression[],
+        readonly equals?: Token
     ) {
         super();
+        this.additionalIndexes ??= [];
         this.range = util.createBoundingRange(
             obj,
             openingSquare,
             index,
             closingSquare,
-            value
+            equals,
+            value,
+            ...this.additionalIndexes
         );
-        this.additionalIndexes ??= [];
     }
 
     public readonly range: Range | undefined;
@@ -1327,7 +1335,9 @@ export class IndexedSetStatement extends Statement {
             }
             result.push(
                 state.transpileToken(this.closingSquare),
-                ' = ',
+                ' ',
+                state.transpileToken(this.equals, '='),
+                ' ',
                 ...this.value.transpile(state)
             );
             return result;
@@ -1351,7 +1361,8 @@ export class IndexedSetStatement extends Statement {
                 this.value?.clone(),
                 util.cloneToken(this.openingSquare),
                 util.cloneToken(this.closingSquare),
-                this.additionalIndexes?.map(e => e?.clone())
+                this.additionalIndexes?.map(e => e?.clone()),
+                util.cloneToken(this.equals)
             ),
             ['obj', 'index', 'value', 'additionalIndexes']
         );
diff --git a/src/parser/TranspileState.ts b/src/parser/TranspileState.ts
index e42d8239..49ea74d1 100644
--- a/src/parser/TranspileState.ts
+++ b/src/parser/TranspileState.ts
@@ -86,7 +86,11 @@ export class TranspileState {
     /**
      * Create a SourceNode from a token, accounting for missing range and multi-line text
      */
-    public transpileToken(token: { range?: Range; text: string }) {
+    public transpileToken(token: { range?: Range; text: string }, defaultValue?: string) {
+        if (!token?.text && defaultValue !== undefined) {
+            return defaultValue;
+        }
+
         if (!token.range) {
             return token.text;
         }


Test patch:
diff --git a/src/parser/tests/expression/TernaryExpression.spec.ts b/src/parser/tests/expression/TernaryExpression.spec.ts
index 3768ffe8..d1f5f0f3 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,129 @@ 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()
+                    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', () => {
+            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 DottedSetStatement to IfStatement', () => {
+            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', () => {
+            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', () => {
+            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', () => {
+            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', () => {
             program.setFile('source/roku_modules/rokucommunity_bslib/bslib.brs', '');
             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 +401,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 +417,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 +433,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 +449,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 +465,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 +483,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 +499,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 +515,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 +531,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 +547,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 +563,11 @@ describe('ternary expressions', () => {
                 end sub
             `, `
                 sub main()
-                    a = bslib_ternary(str("true") = "true", true, false)
+                    if str("true") = "true" then
+                        a = true
+                    else
+                        a = false
+                    end if
                 end sub
             `);
 
@@ -421,7 +577,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 +597,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 +610,22 @@ describe('ternary expressions', () => {
             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 +634,22 @@ describe('ternary expressions', () => {
             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 +658,27 @@ describe('ternary expressions', () => {
             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', () => {
+        it('ignores enum variable names for scope capturing', () => {
             testTranspile(`
                 enum Direction
                     up = "up"
@@ -510,23 +686,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', () => {
+        it('ignores const variable names for scope capturing', () => {
             testTranspile(`
                 enum Direction
                     up = "up"
@@ -535,18 +715,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 +741,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', () => {
+            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', () => {
+            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', () => {
+            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', () => {
+            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 +861,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 +903,6 @@ describe('ternary expressions', () => {
                 `print bslib_ternary(name = "bob", invalid, invalid)`
                 , 'none', undefined, false);
         });
-
     });
 });