Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/astUtils/creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function createIfStatement(options: {
{
if: options.if ?? createToken(TokenKind.If),
then: options.then ?? createToken(TokenKind.Then),
else: options.else ?? createToken(TokenKind.Else),
else: options.elseBranch ? (options.else ?? createToken(TokenKind.Else)) : undefined,
endIf: options.endIf ?? createToken(TokenKind.EndIf)
},
options.condition,
Expand Down
89 changes: 85 additions & 4 deletions src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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 { createAssignmentStatement, createBlock, createDottedSetStatement, createIfStatement, createIndexedSetStatement, createToken, createVariableExpression, createInvalidLiteral } from '../../astUtils/creators';
import { isAssignmentStatement, isBinaryExpression, isBlock, isBody, isBrsFile, isCallExpression, isCallfuncExpression, 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, Statement } from '../../parser/AstNode';
import type { TernaryExpression } from '../../parser/Expression';
import { LiteralExpression } from '../../parser/Expression';
import type { TernaryExpression, NullCoalescingExpression } from '../../parser/Expression';
import { BinaryExpression, LiteralExpression } from '../../parser/Expression';
import { ParseMode } from '../../parser/Parser';
import type { IfStatement } from '../../parser/Statement';
import type { Scope } from '../../Scope';
Expand Down Expand Up @@ -41,6 +41,9 @@ export class BrsFilePreTranspileProcessor {
const visitor = createVisitor({
TernaryExpression: (ternaryExpression) => {
this.processTernaryExpression(ternaryExpression, visitor, walkMode);
},
NullCoalescingExpression: (nullCoalescingExpression) => {
this.processNullCoalescingExpression(nullCoalescingExpression, visitor, walkMode);
}
});
this.event.file.ast.walk(visitor, { walkMode: walkMode });
Expand Down Expand Up @@ -178,6 +181,84 @@ export class BrsFilePreTranspileProcessor {
}
}

private processNullCoalescingExpression(nullCoalescingExpression: NullCoalescingExpression, visitor: ReturnType<typeof createVisitor>, walkMode: WalkMode) {
// Check if this null coalescing expression has complex expressions that require scope protection
const consequentInfo = util.getExpressionInfo(nullCoalescingExpression.consequent, this.event.file);
const alternateInfo = util.getExpressionInfo(nullCoalescingExpression.alternate, this.event.file);

let hasComplexExpression = [
...consequentInfo.expressions,
...alternateInfo.expressions
].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e) || isIndexedGetExpression(e));

// Only optimize if there are no complex expressions
if (hasComplexExpression) {
return;
}
Comment on lines +185 to +197
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid of this junk, we don't need to check for this at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need clarification on this. If I remove the complex expression detection entirely, then expressions like a = user.getAccount() ?? {} would be optimized to if/else instead of falling back to bslib_coalesce(). However, the existing tests expect complex expressions with function calls to use the function-based approach. Should I update the tests to match the new expected behavior, or keep some form of complex expression detection?


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 null coalescing expression is part of a simple assignment to a local variable, rewrite it as an `IfStatement`
let parent = nullCoalescingExpression.findAncestor(x => !isGroupingExpression(x));
let ifStatement: IfStatement;

// Only support simple AssignmentStatement to local variables
if (isAssignmentStatement(parent)) {
// For simple assignments like `a = user ?? {}`, use the original logic
// Create condition: variableName = invalid
const condition = new BinaryExpression(
createVariableExpression(parent.name.text),
createToken(TokenKind.Equal, '=', nullCoalescingExpression.questionQuestionToken.range),
createInvalidLiteral('invalid', nullCoalescingExpression.questionQuestionToken.range)
);

ifStatement = createIfStatement({
if: createToken(TokenKind.If, 'if', nullCoalescingExpression.questionQuestionToken.range),
condition: condition,
then: createToken(TokenKind.Then, 'then', nullCoalescingExpression.questionQuestionToken.range),
thenBranch: createBlock({
statements: [
createAssignmentStatement({
name: parent.name,
equals: parent.equals,
value: nullCoalescingExpression.alternate
})
]
}),
endIf: createToken(TokenKind.EndIf, 'end if', nullCoalescingExpression.questionQuestionToken.range)
});

// First, we need to create the initial assignment statement
const initialAssignment = createAssignmentStatement({
name: parent.name,
equals: parent.equals,
value: nullCoalescingExpression.consequent
});

// Replace the parent with a sequence: first the initial assignment, then the if statement
let { owner, key } = getOwnerAndKey(parent as Statement) ?? {};
if (owner && key !== undefined) {
// Replace with initial assignment first
this.event.editor.setProperty(owner, key, initialAssignment);
// Insert the if statement after
this.event.editor.addToArray(owner, key + 1, ifStatement);
}
}

if (ifStatement) {
//we've injected an ifStatement, so now we need to trigger a walk to handle any nested null coalescing expressions
ifStatement.walk(visitor, { walkMode: walkMode });
}
}

/**
* Given a string optionally separated by dots, find an enum related to it.
* For example, all of these would return the enum: `SomeNamespace.SomeEnum.SomeMember`, SomeEnum.SomeMember, `SomeEnum`
Expand Down
108 changes: 104 additions & 4 deletions src/parser/tests/expression/NullCoalescenceExpression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ describe('NullCoalescingExpression', () => {
end sub
`, `
sub main()
a = rokucommunity_bslib_coalesce(user, false)
a = user
if a = invalid then
a = false
end if
end sub
`);
});
Expand All @@ -209,9 +212,12 @@ describe('NullCoalescingExpression', () => {
end sub
`, `
sub main()
a = bslib_coalesce(user, {
"id": "default"
})
a = user
if a = invalid then
a = {
"id": "default"
}
end if
end sub
`);
});
Expand Down Expand Up @@ -374,5 +380,99 @@ describe('NullCoalescingExpression', () => {
end sub
`);
});

it('transpiles null coalescing in RHS of AssignmentStatement to if statement', () => {
testTranspile(`
sub main()
a = user ?? {}
end sub
`, `
sub main()
a = user
if a = invalid then
a = {}
end if
end sub
`);
});

it('uses bslib_coalesce for compound assignment operators', () => {
testTranspile(`
sub main()
a += user ?? 0
end sub
`, `
sub main()
a += bslib_coalesce(user, 0)
end sub
`);
});

it('supports nested null coalescing in assignment', () => {
testTranspile(`
sub main()
result = user ?? (fallback ?? {})
end sub
`, `
sub main()
result = user
if result = invalid then
result = fallback
if result = invalid then
result = {}
end if
end if
end sub
`);
});

it('uses scope-captured functions for DottedSet expressions', () => {
testTranspile(`
sub main()
m.a = user ?? {}
end sub
`, `
sub main()
m.a = bslib_coalesce(user, {})
end sub
`);
});

it('uses scope-captured functions for IndexedSet expressions', () => {
testTranspile(`
sub main()
m["a"] = user ?? {}
end sub
`, `
sub main()
m["a"] = bslib_coalesce(user, {})
end sub
`);
});

it('uses scope-captured functions for complex expressions', () => {
testTranspile(`
sub main()
zombie = {}
result = [
zombie.getName() ?? "zombie"
]
end sub
`, `
sub main()
zombie = {}
result = [
(function(zombie)
__bsConsequent = zombie.getName()
if __bsConsequent <> invalid then
return __bsConsequent
else
return "zombie"
end if
end function)(zombie)
]
end sub
`);
});
});
});