Skip to content

Refactor expression evaluator #57955

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

Merged
merged 2 commits into from
Mar 27, 2024
Merged
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
150 changes: 37 additions & 113 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import {
createDiagnosticForNodeFromMessageChain,
createDiagnosticMessageChainFromDiagnostic,
createEmptyExports,
createEvaluator,
createFileDiagnostic,
createGetCanonicalFileName,
createGetSymbolWalker,
Expand Down Expand Up @@ -1474,6 +1475,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var checkBinaryExpression = createCheckBinaryExpression();
var emitResolver = createResolver();
var nodeBuilder = createNodeBuilder();
var evaluate = createEvaluator({
evaluateElementAccessExpression,
evaluateEntityNameExpression,
});
Copy link
Member

Choose a reason for hiding this comment

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

It feels strange to have this callback. This only needs the AST node and nothing else. Can we just walk the nodes like other grammar checks?

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 was trying to introduce as little extra overhead as possible, I agree that callback is kinds of the odd one out, but without an extra walk I'm not sure how we could ensure that check is performed in the same way.

The check does appear to happen in other places though. Enum members should go through checkEnumMember which will call checkGrammarNumericLiteral during expression checking. Similarly for template literals. So the check might be redundant for a full check. I'll test and see.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just tested .. removing the check from here does not fail any test 😕. I do think it ends up being called through other paths.


var globals = createSymbolTable();
var undefinedSymbol = createSymbol(SymbolFlags.Property, "undefined" as __String);
Expand Down Expand Up @@ -39279,7 +39284,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node, /*contextFlags*/ undefined) || unknownType, isTemplateLiteralContextualType)) {
return getTemplateLiteralType(texts, types);
}
const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluateTemplateExpression(node);
const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node);
return evaluated ? getFreshTypeOfLiteralType(getStringLiteralType(evaluated)) : stringType;
}

Expand Down Expand Up @@ -45773,108 +45778,40 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}

function evaluate(expr: Expression, location?: Declaration): string | number | undefined {
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
const value = evaluate((expr as PrefixUnaryExpression).operand, location);
if (typeof value === "number") {
switch ((expr as PrefixUnaryExpression).operator) {
case SyntaxKind.PlusToken:
return value;
case SyntaxKind.MinusToken:
return -value;
case SyntaxKind.TildeToken:
return ~value;
}
}
break;
case SyntaxKind.BinaryExpression:
const left = evaluate((expr as BinaryExpression).left, location);
const right = evaluate((expr as BinaryExpression).right, location);
if (typeof left === "number" && typeof right === "number") {
switch ((expr as BinaryExpression).operatorToken.kind) {
case SyntaxKind.BarToken:
return left | right;
case SyntaxKind.AmpersandToken:
return left & right;
case SyntaxKind.GreaterThanGreaterThanToken:
return left >> right;
case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return left >>> right;
case SyntaxKind.LessThanLessThanToken:
return left << right;
case SyntaxKind.CaretToken:
return left ^ right;
case SyntaxKind.AsteriskToken:
return left * right;
case SyntaxKind.SlashToken:
return left / right;
case SyntaxKind.PlusToken:
return left + right;
case SyntaxKind.MinusToken:
return left - right;
case SyntaxKind.PercentToken:
return left % right;
case SyntaxKind.AsteriskAsteriskToken:
return left ** right;
}
}
else if (
(typeof left === "string" || typeof left === "number") &&
(typeof right === "string" || typeof right === "number") &&
(expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken
) {
return "" + left + right;
}
break;
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return (expr as StringLiteralLike).text;
case SyntaxKind.TemplateExpression:
return evaluateTemplateExpression(expr as TemplateExpression, location);
case SyntaxKind.NumericLiteral:
checkGrammarNumericLiteral(expr as NumericLiteral);
return +(expr as NumericLiteral).text;
case SyntaxKind.ParenthesizedExpression:
return evaluate((expr as ParenthesizedExpression).expression, location);
case SyntaxKind.Identifier: {
const identifier = expr as Identifier;
if (isInfinityOrNaNString(identifier.escapedText) && (resolveEntityName(identifier, SymbolFlags.Value, /*ignoreErrors*/ true) === getGlobalSymbol(identifier.escapedText, SymbolFlags.Value, /*diagnostic*/ undefined))) {
return +(identifier.escapedText);
}
// falls through
function evaluateEntityNameExpression(expr: EntityNameExpression, location?: Declaration) {
const symbol = resolveEntityName(expr, SymbolFlags.Value, /*ignoreErrors*/ true);
if (!symbol) return undefined;

if (expr.kind === SyntaxKind.Identifier) {
const identifier = expr;
if (isInfinityOrNaNString(identifier.escapedText) && (symbol === getGlobalSymbol(identifier.escapedText, SymbolFlags.Value, /*diagnostic*/ undefined))) {
return +(identifier.escapedText);
}
case SyntaxKind.PropertyAccessExpression:
if (isEntityNameExpression(expr)) {
const symbol = resolveEntityName(expr, SymbolFlags.Value, /*ignoreErrors*/ true);
if (symbol) {
if (symbol.flags & SymbolFlags.EnumMember) {
return location ? evaluateEnumMember(expr, symbol, location) : getEnumMemberValue(symbol.valueDeclaration as EnumMember);
}
if (isConstantVariable(symbol)) {
const declaration = symbol.valueDeclaration;
if (declaration && isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && (!location || declaration !== location && isBlockScopedNameDeclaredBeforeUse(declaration, location))) {
return evaluate(declaration.initializer, declaration);
}
}
}
}
break;
case SyntaxKind.ElementAccessExpression:
const root = (expr as ElementAccessExpression).expression;
if (isEntityNameExpression(root) && isStringLiteralLike((expr as ElementAccessExpression).argumentExpression)) {
const rootSymbol = resolveEntityName(root, SymbolFlags.Value, /*ignoreErrors*/ true);
if (rootSymbol && rootSymbol.flags & SymbolFlags.Enum) {
const name = escapeLeadingUnderscores(((expr as ElementAccessExpression).argumentExpression as StringLiteralLike).text);
const member = rootSymbol.exports!.get(name);
if (member) {
return location ? evaluateEnumMember(expr, member, location) : getEnumMemberValue(member.valueDeclaration as EnumMember);
}
}
}

if (symbol.flags & SymbolFlags.EnumMember) {
return location ? evaluateEnumMember(expr, symbol, location) : getEnumMemberValue(symbol.valueDeclaration as EnumMember);
}
if (isConstantVariable(symbol)) {
const declaration = symbol.valueDeclaration;
if (declaration && isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && (!location || declaration !== location && isBlockScopedNameDeclaredBeforeUse(declaration, location))) {
return evaluate(declaration.initializer, declaration);
}
}
}

function evaluateElementAccessExpression(expr: ElementAccessExpression, location?: Declaration) {
const root = expr.expression;
if (isEntityNameExpression(root) && isStringLiteralLike(expr.argumentExpression)) {
const rootSymbol = resolveEntityName(root, SymbolFlags.Value, /*ignoreErrors*/ true);
if (rootSymbol && rootSymbol.flags & SymbolFlags.Enum) {
const name = escapeLeadingUnderscores(expr.argumentExpression.text);
const member = rootSymbol.exports!.get(name);
if (member) {
return location ? evaluateEnumMember(expr, member, location) : getEnumMemberValue(member.valueDeclaration as EnumMember);
}
break;
}
}
return undefined;
}

function evaluateEnumMember(expr: Expression, symbol: Symbol, location: Declaration) {
Expand All @@ -45890,19 +45827,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getEnumMemberValue(declaration as EnumMember);
}

function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration) {
let result = expr.head.text;
for (const span of expr.templateSpans) {
const value = evaluate(span.expression, location);
if (value === undefined) {
return undefined;
}
result += value;
result += span.literal.text;
}
return result;
}

function checkEnumDeclaration(node: EnumDeclaration) {
addLazyDiagnostic(() => checkEnumDeclarationWorker(node));
}
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10056,3 +10056,9 @@ export interface Queue<T> {
dequeue(): T;
isEmpty(): boolean;
}

/** @internal */
export interface EvaluationResolver {
evaluateEntityNameExpression(expr: EntityNameExpression, location: Declaration | undefined): string | number | undefined;
evaluateElementAccessExpression(expr: ElementAccessExpression, location: Declaration | undefined): string | number | undefined;
}
97 changes: 97 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import {
EqualsToken,
equateValues,
escapeLeadingUnderscores,
EvaluationResolver,
every,
ExportAssignment,
ExportDeclaration,
Expand Down Expand Up @@ -511,6 +512,7 @@ import {
SyntaxKind,
SyntaxList,
TaggedTemplateExpression,
TemplateExpression,
TemplateLiteral,
TemplateLiteralLikeNode,
TemplateLiteralToken,
Expand Down Expand Up @@ -10664,3 +10666,98 @@ export function isSyntacticallyString(expr: Expression): boolean {
}
return false;
}

/** @internal */
export function createEvaluator({ evaluateElementAccessExpression, evaluateEntityNameExpression }: EvaluationResolver) {
function evaluate(expr: TemplateExpression, location?: Declaration): string;
function evaluate(expr: Expression, location?: Declaration): string | number | undefined;
function evaluate(expr: Expression, location?: Declaration): string | number | undefined {
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
const value = evaluate((expr as PrefixUnaryExpression).operand, location);
if (typeof value === "number") {
switch ((expr as PrefixUnaryExpression).operator) {
case SyntaxKind.PlusToken:
return value;
case SyntaxKind.MinusToken:
return -value;
case SyntaxKind.TildeToken:
return ~value;
}
}
break;
case SyntaxKind.BinaryExpression:
const left = evaluate((expr as BinaryExpression).left, location);
const right = evaluate((expr as BinaryExpression).right, location);
if (typeof left === "number" && typeof right === "number") {
switch ((expr as BinaryExpression).operatorToken.kind) {
case SyntaxKind.BarToken:
return left | right;
case SyntaxKind.AmpersandToken:
return left & right;
case SyntaxKind.GreaterThanGreaterThanToken:
return left >> right;
case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return left >>> right;
case SyntaxKind.LessThanLessThanToken:
return left << right;
case SyntaxKind.CaretToken:
return left ^ right;
case SyntaxKind.AsteriskToken:
return left * right;
case SyntaxKind.SlashToken:
return left / right;
case SyntaxKind.PlusToken:
return left + right;
case SyntaxKind.MinusToken:
return left - right;
case SyntaxKind.PercentToken:
return left % right;
case SyntaxKind.AsteriskAsteriskToken:
return left ** right;
}
}
else if (
(typeof left === "string" || typeof left === "number") &&
(typeof right === "string" || typeof right === "number") &&
(expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken
) {
return "" + left + right;
}
break;
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return (expr as StringLiteralLike).text;
case SyntaxKind.TemplateExpression:
return evaluateTemplateExpression(expr as TemplateExpression, location);
case SyntaxKind.NumericLiteral:
return +(expr as NumericLiteral).text;
case SyntaxKind.ParenthesizedExpression:
return evaluate((expr as ParenthesizedExpression).expression, location);
case SyntaxKind.Identifier:
return evaluateEntityNameExpression(expr as Identifier, location);
case SyntaxKind.PropertyAccessExpression:
if (isEntityNameExpression(expr)) {
return evaluateEntityNameExpression(expr, location);
}
break;
case SyntaxKind.ElementAccessExpression:
return evaluateElementAccessExpression(expr as ElementAccessExpression, location);
}
return undefined;
}

function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration) {
let result = expr.head.text;
for (const span of expr.templateSpans) {
const value = evaluate(span.expression, location);
if (value === undefined) {
return undefined;
}
result += value;
result += span.literal.text;
}
return result;
}
return evaluate;
}