Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors PrintStatement so that .values array is all expressions #1296

Merged
merged 8 commits into from
Jan 3, 2025
5 changes: 4 additions & 1 deletion src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Body, AssignmentStatement, Block, ExpressionStatement, ExitStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, TypecastStatement, AliasStatement, ConditionalCompileStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, AugmentedAssignmentStatement, DimStatement } from '../parser/Statement';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypecastExpression, TypeExpression, TypedArrayExpression, TernaryExpression, NullCoalescingExpression } from '../parser/Expression';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypecastExpression, TypeExpression, TypedArrayExpression, TernaryExpression, NullCoalescingExpression, PrintSeparatorExpression } from '../parser/Expression';
import type { BrsFile } from '../files/BrsFile';
import type { XmlFile } from '../files/XmlFile';
import type { BsDiagnostic, TypedefProvider } from '../interfaces';
Expand Down Expand Up @@ -304,6 +304,9 @@ export function isTypecastExpression(element: any): element is TypecastExpressio
export function isTypedArrayExpression(element: any): element is TypedArrayExpression {
return element?.kind === AstNodeKind.TypedArrayExpression;
}
export function isPrintSeparatorExpression(element: any): element is PrintSeparatorExpression {
return element?.kind === AstNodeKind.PrintSeparatorExpression;
}

// BscType reflection
export function isStringType(value: any): value is StringType {
Expand Down
1 change: 1 addition & 0 deletions src/astUtils/visitors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ describe('astUtils visitors', () => {
program.setFile('source/main.brs', EXPRESSIONS_SRC);
expect(actual).to.deep.equal([
'PrintStatement:1:LiteralExpression', // print <"msg">; 3
'PrintStatement:1:PrintSeparatorExpression', // print "msg"<;> 3
'PrintStatement:1:LiteralExpression', // print "msg"; <3>
'PrintStatement:1:TemplateStringExpression', // print <`expand ${var}`>
'PrintStatement:1:TemplateStringQuasiExpression', // print `<expand >${var}`
Expand Down
9 changes: 9 additions & 0 deletions src/lexer/TokenKind.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Token } from './Token';

export enum TokenKind {
// parens (and friends)
LeftParen = 'LeftParen', // (
Expand Down Expand Up @@ -711,6 +713,13 @@ export const AllowedTriviaTokens: ReadonlyArray<TokenKind> = [
];


/**
* Tokens that can be used between expressions in a print statement
*/
export type PrintSeparatorToken = Token & {
kind: TokenKind.Comma | TokenKind.Semicolon;
};

/**
* The tokens that may be in a binary expression
*/
Expand Down
3 changes: 2 additions & 1 deletion src/parser/AstNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,5 +324,6 @@ export enum AstNodeKind {
ConditionalCompileStatement = 'ConditionalCompileStatement',
ConditionalCompileConstStatement = 'ConditionalCompileConstStatement',
ConditionalCompileErrorStatement = 'ConditionalCompileErrorStatement',
AugmentedAssignmentStatement = 'AugmentedAssignmentStatement'
AugmentedAssignmentStatement = 'AugmentedAssignmentStatement',
PrintSeparatorExpression = 'PrintSeparatorExpression'
}
47 changes: 47 additions & 0 deletions src/parser/Expression.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-bitwise */
import type { Token, Identifier } from '../lexer/Token';
import type { PrintSeparatorToken } from '../lexer/TokenKind';
import { TokenKind } from '../lexer/TokenKind';
import type { Block, NamespaceStatement } from './Statement';
import type { Location } from 'vscode-languageserver';
Expand Down Expand Up @@ -966,6 +967,52 @@ export class LiteralExpression extends Expression {
}
}

/**
* The print statement can have a mix of expressions and separators. These separators represent actual output to the screen,
* so this AstNode represents those separators (comma, semicolon, and whitespace)
*/
export class PrintSeparatorExpression extends Expression {
constructor(options: {
separator: PrintSeparatorToken;
}) {
super();
this.tokens = {
separator: options.separator
};
this.location = this.tokens.separator.location;
}

public readonly tokens: {
readonly separator: PrintSeparatorToken;
};

public readonly kind = AstNodeKind.PrintSeparatorExpression;

public location: Location;

transpile(state: BrsTranspileState) {
return [
...this.tokens.separator.leadingWhitespace ?? [],
...state.transpileToken(this.tokens.separator)
];
}

walk(visitor: WalkVisitor, options: WalkOptions) {
//nothing to walk
}

get leadingTrivia(): Token[] {
return this.tokens.separator.leadingTrivia;
}

public clone() {
return new PrintSeparatorExpression({
separator: util.cloneToken(this.tokens?.separator)
});
}
}


/**
* This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
* during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
Expand Down
20 changes: 6 additions & 14 deletions src/parser/Parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Token, Identifier } from '../lexer/Token';
import { isToken } from '../lexer/Token';
import type { BlockTerminator } from '../lexer/TokenKind';
import type { BlockTerminator, PrintSeparatorToken } from '../lexer/TokenKind';
import { Lexer } from '../lexer/Lexer';
import {
AllowedLocalIdentifiers,
Expand All @@ -17,10 +17,6 @@ import {
CompoundAssignmentOperators,
BinaryExpressionOperatorTokens
} from '../lexer/TokenKind';
import type {
PrintSeparatorSpace,
PrintSeparatorTab
} from './Statement';
import {
AliasStatement,
AssignmentStatement,
Expand Down Expand Up @@ -96,7 +92,8 @@ import {
TypedArrayExpression,
UnaryExpression,
VariableExpression,
XmlAttributeGetExpression
XmlAttributeGetExpression,
PrintSeparatorExpression
} from './Expression';
import type { Range } from 'vscode-languageserver';
import type { Logger } from '../logging';
Expand Down Expand Up @@ -2441,16 +2438,11 @@ export class Parser {
private printStatement(): PrintStatement {
let printKeyword = this.advance();

let values: (
| Expression
| PrintSeparatorTab
| PrintSeparatorSpace)[] = [];
let values: Expression[] = [];

while (!this.checkEndOfStatement()) {
if (this.check(TokenKind.Semicolon)) {
values.push(this.advance() as PrintSeparatorSpace);
} else if (this.check(TokenKind.Comma)) {
values.push(this.advance() as PrintSeparatorTab);
if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
} else if (this.check(TokenKind.Else)) {
break; // inline branch
} else {
Expand Down
64 changes: 28 additions & 36 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import type { BrsTranspileState } from './BrsTranspileState';
import { ParseMode } from './Parser';
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
import { isCallExpression, isCatchStatement, isConditionalCompileStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isVoidType, isWhileStatement } from '../astUtils/reflection';
import { isCallExpression, isCatchStatement, isConditionalCompileStatement, isEnumMemberStatement, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isPrintSeparatorExpression, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isVoidType, isWhileStatement } from '../astUtils/reflection';
import { TypeChainEntry, type GetTypeOptions, type TranspileResult, type TypedefProvider } from '../interfaces';
import { createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators';
import { DynamicType } from '../types/DynamicType';
import type { BscType } from '../types/BscType';
import { SymbolTable } from '../SymbolTable';
import type { AstNode, Expression } from './AstNode';
import type { Expression } from './AstNode';
import { AstNodeKind, Statement } from './AstNode';
import { ClassType } from '../types/ClassType';
import { EnumMemberType, EnumType } from '../types/EnumType';
Expand Down Expand Up @@ -878,16 +878,6 @@ export class IncrementStatement extends Statement {
}
}

/** Used to indent the current `print` position to the next 16-character-width output zone. */
export interface PrintSeparatorTab extends Token {
kind: TokenKind.Comma;
}

/** Used to insert a single whitespace character at the current `print` position. */
export interface PrintSeparatorSpace extends Token {
kind: TokenKind.Semicolon;
}

/**
* Represents a `print` statement within BrightScript.
*/
Expand All @@ -896,11 +886,11 @@ export class PrintStatement extends Statement {
* Creates a new internal representation of a BrightScript `print` statement.
* @param options the options for this statement
* @param options.print a print token
* @param options.expressions an array of expressions or `PrintSeparator`s to be evaluated and printed.
* @param options.expressions an array of expressions to be evaluated and printed. Wrap PrintSeparator tokens (`;` or `,`) in `PrintSeparatorExpression`
*/
constructor(options: {
print?: Token;
expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;
expressions: Array<Expression>;
}) {
super();
this.tokens = {
Expand All @@ -912,40 +902,48 @@ export class PrintStatement extends Statement {
...(this.expressions ?? [])
);
}

public readonly tokens: {
readonly print?: Token;
};
public readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;

public readonly expressions: Array<Expression>;

public readonly kind = AstNodeKind.PrintStatement;

public readonly location: Location | undefined;

transpile(state: BrsTranspileState) {
let result = [
state.transpileToken(this.tokens.print, 'print'),
' '
state.transpileToken(this.tokens.print, 'print')
] as TranspileResult;

//if the first expression has no leading whitespace, add a single space between the `print` and the expression
if (this.expressions.length > 0 && !this.expressions[0].leadingTrivia.find(t => t.kind === TokenKind.Whitespace)) {
result.push(' ');
}

// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < this.expressions.length; i++) {
const expressionOrSeparator: any = this.expressions[i];
if (expressionOrSeparator.transpile) {
result.push(...(expressionOrSeparator as ExpressionStatement).transpile(state));
} else {
result.push(
state.tokenToSourceNode(expressionOrSeparator)
);
}
//if there's an expression after us, add a space
if ((this.expressions[i + 1] as any)?.transpile) {
const expression = this.expressions[i];
let leadingWhitespace = expression.leadingTrivia.find(t => t.kind === TokenKind.Whitespace)?.text;
if (leadingWhitespace) {
result.push(leadingWhitespace);
//if the previous expression was NOT a separator, and this one is not also, add a space between them
} else if (i > 0 && !isPrintSeparatorExpression(this.expressions[i - 1]) && !isPrintSeparatorExpression(expression) && !leadingWhitespace) {
result.push(' ');
}

result.push(
...expression.transpile(state)
);
}
return result;
}

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
//sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
walkArray(this.expressions as AstNode[], visitor, options, this, (item) => isExpression(item as any));
walkArray(this.expressions, visitor, options, this);
}
}

Expand All @@ -957,13 +955,7 @@ export class PrintStatement extends Statement {
return this.finalizeClone(
new PrintStatement({
print: util.cloneToken(this.tokens.print),
expressions: this.expressions?.map(e => {
if (isExpression(e as any)) {
return (e as Expression).clone();
} else {
return util.cloneToken(e as Token);
}
})
expressions: this.expressions?.map(e => e?.clone())
}),
['expressions' as any]
);
Expand Down
6 changes: 3 additions & 3 deletions src/parser/tests/statement/PrintStatement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ describe('parser print statements', () => {
x = 5
print 25; " is equal to"; x ^ 2
a$ = "string"
print a$; a$, a$; " "; a$
print "zone 1", "zone 2", "zone 3", "zone 4"
print a$;a$,a$;" ";a$
print "zone 1","zone 2","zone 3","zone 4"
print "print statement #1 "
print "print statement #2"
print "this is a five " 5 "!!"
Expand All @@ -202,7 +202,7 @@ describe('parser print statements', () => {
print [
5
]
print tab(5) "tabbed 5"; tab(25) "tabbed 25"
print tab(5) "tabbed 5";tab(25) "tabbed 25"
print tab(40) pos(0) 'prints 40 at position 40
print "these" tab(pos(0) + 5) "words" tab(pos(0) + 5) "are"
print tab(pos(0) + 5) "evenly" tab(pos(0) + 5) "spaced"
Expand Down
Loading