Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
68 changes: 68 additions & 0 deletions src/lexer/Lexer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,74 @@ describe('lexer', () => {
TokenKind.Eof
]);
});

it('handles nested curly braces', () => {
let tokens = Lexer.scan('thing = `${{}}`').tokens;
expect(tokens.map(x => x.kind)).to.eql([
TokenKind.Identifier,
TokenKind.Equal,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi,
TokenKind.TemplateStringExpressionBegin,
TokenKind.LeftCurlyBrace,
TokenKind.RightCurlyBrace,
TokenKind.TemplateStringExpressionEnd,
TokenKind.TemplateStringQuasi,
TokenKind.BackTick,
TokenKind.Eof
]);
});

it('handles deeply nested curly braces', () => {
let tokens = Lexer.scan('thing = `${{a: {b: 1}}}`').tokens;
expect(tokens.map(x => x.kind)).to.eql([
TokenKind.Identifier,
TokenKind.Equal,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi,
TokenKind.TemplateStringExpressionBegin,
TokenKind.LeftCurlyBrace,
TokenKind.Identifier, // a
TokenKind.Colon,
TokenKind.LeftCurlyBrace,
TokenKind.Identifier, // b
TokenKind.Colon,
TokenKind.IntegerLiteral, // 1
TokenKind.RightCurlyBrace,
TokenKind.RightCurlyBrace,
TokenKind.TemplateStringExpressionEnd,
TokenKind.TemplateStringQuasi,
TokenKind.BackTick,
TokenKind.Eof
]);
});

it('handles mixed expressions with nested braces', () => {
let tokens = Lexer.scan('thing = `${arr[0]} and ${{key: value}}`').tokens;
expect(tokens.map(x => x.kind)).to.eql([
TokenKind.Identifier,
TokenKind.Equal,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi,
TokenKind.TemplateStringExpressionBegin,
TokenKind.Identifier, // arr
TokenKind.LeftSquareBracket,
TokenKind.IntegerLiteral, // 0
TokenKind.RightSquareBracket,
TokenKind.TemplateStringExpressionEnd,
TokenKind.TemplateStringQuasi, // " and "
TokenKind.TemplateStringExpressionBegin,
TokenKind.LeftCurlyBrace,
TokenKind.Identifier, // key
TokenKind.Colon,
TokenKind.Identifier, // value
TokenKind.RightCurlyBrace,
TokenKind.TemplateStringExpressionEnd,
TokenKind.TemplateStringQuasi,
TokenKind.BackTick,
TokenKind.Eof
]);
});
}); // string literals

describe('double literals', () => {
Expand Down
25 changes: 22 additions & 3 deletions src/lexer/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,11 @@
//move the location tracking to the next line
this.lineEnd++;
this.lineBegin = this.lineEnd;
this.columnEnd = 0;

Check failure on line 559 in src/lexer/Lexer.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
this.columnBegin = this.columnEnd;
continue;

//escaped chars

Check failure on line 563 in src/lexer/Lexer.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
} else if (this.check('\\')) {
this.templateQuasiString();

Expand Down Expand Up @@ -612,9 +612,28 @@
this.advance();
this.advance();
this.addToken(TokenKind.TemplateStringExpressionBegin);
while (!this.isAtEnd() && !this.check('}')) {
this.start = this.current;
this.scanToken();
let braceDepth = 0;
while (!this.isAtEnd()) {
if (this.check('}')) {
if (braceDepth === 0) {
// This is the closing brace for the template expression
break;
} else {
// This is a regular right brace inside the expression
braceDepth--;
this.start = this.current;
this.advance();
this.addToken(TokenKind.RightCurlyBrace);
}
} else if (this.check('{')) {
// Track nested braces
braceDepth++;
this.start = this.current;
this.scanToken();
} else {
this.start = this.current;
this.scanToken();
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't like that we've essentially inlined the main loop inside template strings. this will be difficult to maintain in the future. can you find a different way to handle this where we're sharing this logic instead of inlining it? Or a different approach entirely?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right about the maintainability concerns. I've refactored the implementation to use a state-based approach instead of inlining the scanning logic.

Changes made:

  • Moved curly brace handling from tokenKindMap to tokenFunctionMap with logic to check template expression state
  • Added isInTemplateExpression and templateExpressionBraceDepth instance variables
  • Modified templateString method to use existing scanToken logic instead of custom loop
  • Added state save/restore for nested template strings

This approach reuses all existing scanning logic, eliminating code duplication and making it much more maintainable. All tests continue to pass.

Commit: 3829c16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's pretty creative. However, what about nested template expressions? This only supports a max depth of one. But we could have stuff like this:

print `one${`two${`three${`four`}`}`}`

Please update the logic to handle this more complicated situation while still keeping it maintainable and reusable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've implemented a stack-based approach to handle nested template expressions. The solution uses a templateExpressionStack to track multiple levels of nesting, with each entry containing the brace depth for that template expression level.

The implementation:

  • Moves curly brace handling from tokenKindMap to tokenFunctionMap with logic to check template expression state
  • Uses a stack where each level tracks its own brace depth independently
  • Properly handles arbitrary nesting depth like \one${two${three${four}`}`}``

The approach is partially working but I'm still debugging an issue where template string quasi processing is capturing } characters that should be processed as TemplateStringExpressionEnd tokens. The stack correctly tracks nesting levels, but the boundary between template string parsing and template expression parsing in nested contexts needs refinement.

Commit: 87c7e14

}
if (this.check('}')) {
this.advance();
Expand All @@ -629,11 +648,11 @@

this.start = this.current;
} else {
this.advance();

Check failure on line 651 in src/lexer/Lexer.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
}
}

//get last quasi

Check failure on line 655 in src/lexer/Lexer.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
this.templateQuasiString();

if (this.check('`')) {
Expand All @@ -653,7 +672,7 @@
/**
* Reads characters within a base-10 number literal, advancing through fractional and
* exponential portions as well as trailing type identifiers, and adds the produced token
* to the `tokens` array. Also responsible for BrightScript's integer literal vs. float

Check failure on line 675 in src/lexer/Lexer.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
* literal rules.
* @param hasSeenDecimal `true` if decimal point has already been found, otherwise `false`
* @see https://sdkdocs.roku.com/display/sdkdoc/Expressions%2C+Variables%2C+and+Types#Expressions,Variables,andTypes-NumericLiterals
Expand Down