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
94 changes: 94 additions & 0 deletions src/lexer/Lexer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,100 @@ 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
]);
});

it('handles nested template expressions', () => {
let tokens = Lexer.scan('print `one${`two${`three${`four`}`}`}`').tokens;
expect(tokens.map(x => x.kind)).to.eql([
TokenKind.Print,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi, // one
TokenKind.TemplateStringExpressionBegin,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi, // two
TokenKind.TemplateStringExpressionBegin,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi, // three
TokenKind.TemplateStringExpressionBegin,
TokenKind.BackTick,
TokenKind.TemplateStringQuasi, // four
TokenKind.BackTick,
TokenKind.TemplateStringExpressionEnd,
TokenKind.BackTick,
TokenKind.TemplateStringExpressionEnd,
TokenKind.BackTick,
TokenKind.TemplateStringExpressionEnd,
TokenKind.BackTick,
TokenKind.Eof
]);
});
}); // string literals

describe('double literals', () => {
Expand Down
71 changes: 58 additions & 13 deletions src/lexer/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ export class Lexer {
*/
private leadingWhitespace = '';

/**
* Stack to track nested template string expression state.
* Each entry contains the brace depth for that template expression level.
* Empty stack means we're not in any template expression.
*/
private templateExpressionStack: number[] = [];

/**
* Returns true if we're currently inside any template string expression
*/
private get isInTemplateExpression(): boolean {
return this.templateExpressionStack.length > 0;
}

/**
* Returns the current template expression brace depth (0 if not in template expression)
*/
private get templateExpressionBraceDepth(): number {
return this.templateExpressionStack.length > 0
? this.templateExpressionStack[this.templateExpressionStack.length - 1]
: 0;
}

/**
* Sets the current template expression brace depth
*/
private set templateExpressionBraceDepth(depth: number) {
if (this.templateExpressionStack.length > 0) {
this.templateExpressionStack[this.templateExpressionStack.length - 1] = depth;
}
}

/**
* A convenience function, equivalent to `new Lexer().scan(toScan)`, that converts a string
* containing BrightScript code to an array of `Token` objects that will later be used to build
Expand Down Expand Up @@ -147,6 +179,27 @@ export class Lexer {
'"': Lexer.prototype.string,
'\'': Lexer.prototype.comment,
'`': Lexer.prototype.templateString,
'{': function (this: Lexer) {
if (this.isInTemplateExpression) {
this.templateExpressionBraceDepth++;
}
this.addToken(TokenKind.LeftCurlyBrace);
},
'}': function (this: Lexer) {
if (this.isInTemplateExpression) {
if (this.templateExpressionBraceDepth > 0) {
this.templateExpressionBraceDepth--;
this.addToken(TokenKind.RightCurlyBrace);
} else {
// This is the closing brace for the template expression
// Pop the current template expression level from the stack
this.templateExpressionStack.pop();
this.addToken(TokenKind.TemplateStringExpressionEnd);
}
} else {
this.addToken(TokenKind.RightCurlyBrace);
}
},
'.': function (this: Lexer) {
// this might be a float/double literal, because decimals without a leading 0
// are allowed
Expand Down Expand Up @@ -332,8 +385,6 @@ export class Lexer {
')': TokenKind.RightParen,
'=': TokenKind.Equal,
',': TokenKind.Comma,
'{': TokenKind.LeftCurlyBrace,
'}': TokenKind.RightCurlyBrace,
'[': TokenKind.LeftSquareBracket,
']': TokenKind.RightSquareBracket,
'^': TokenKind.Caret,
Expand Down Expand Up @@ -612,20 +663,14 @@ export class Lexer {
this.advance();
this.advance();
this.addToken(TokenKind.TemplateStringExpressionBegin);
while (!this.isAtEnd() && !this.check('}')) {

// Enter template expression mode by pushing a new level onto the stack
this.templateExpressionStack.push(0);

while (!this.isAtEnd() && this.isInTemplateExpression) {
this.start = this.current;
this.scanToken();
}
if (this.check('}')) {
this.advance();
this.addToken(TokenKind.TemplateStringExpressionEnd);
} else {

this.diagnostics.push({
...DiagnosticMessages.unexpectedConditionalCompilationString(),
range: this.rangeOf()
});
}

this.start = this.current;
} else {
Expand Down