Skip to content

feat(compiler): evaluate static interpolations at compile time #13617

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

CamWass
Copy link

@CamWass CamWass commented Jul 11, 2025

This PR allows the compiler to evaluate static interpolations (and calls to toDisplayString) at compile time. 'Static' here means that the value is marked as ConstantTypes.CAN_STRINGIFY.

I found calls to toDisplayString with literal arguments in a real-world, large Vue project. These can be optimised at compile time with little effort, removing a function call from the generated code and improving performance + code-size slightly.

This PR:

  • Marks empty interpolations, or interpolation with undefined, as static and handles them when evaluating at compile time. This allows them to be stringified.
  • Evaluates static calls to toDisplayString during codegen. This catches static calls that couldn't be strinngified (most).

Example input:

<template>
  <div>{{ "1" }}</div>
  <div>{{ `1` }}</div>
  <div>{{ 1 }}</div>
  <div>{{ false }}</div>
  <div>{{ null }}</div>
  <div>{{ undefined }}</div>
  <div>{{ }}</div>
</template>

Output before this PR (plaground link):

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _cache[0] || (_cache[0] = _createElementVNode("div", null, _toDisplayString("1"), -1 /* CACHED */)),
    _cache[1] || (_cache[1] = _createElementVNode("div", null, _toDisplayString(`1`), -1 /* CACHED */)),
    _cache[2] || (_cache[2] = _createElementVNode("div", null, _toDisplayString(1), -1 /* CACHED */)),
    _cache[3] || (_cache[3] = _createElementVNode("div", null, _toDisplayString(false), -1 /* CACHED */)),
    _cache[4] || (_cache[4] = _createElementVNode("div", null, _toDisplayString(null), -1 /* CACHED */)),
    _cache[5] || (_cache[5] = _createElementVNode("div", null, _toDisplayString(undefined), -1 /* CACHED */)),
    _createElementVNode("div", null, _toDisplayString(), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}

Output after this PR:

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _cache[0] || (_cache[0] = _createElementVNode("div", null, "1", -1 /* CACHED */)),
    _cache[1] || (_cache[1] = _createElementVNode("div", null, "1", -1 /* CACHED */)),
    _cache[2] || (_cache[2] = _createElementVNode("div", null, "1", -1 /* CACHED */)),
    _cache[3] || (_cache[3] = _createElementVNode("div", null, "false", -1 /* CACHED */)),
    _cache[4] || (_cache[4] = _createElementVNode("div", null, "", -1 /* CACHED */)),
    _cache[5] || (_cache[5] = _createElementVNode("div", null, "", -1 /* CACHED */)),
    _cache[6] || (_cache[6] = _createElementVNode("div", null, "", -1 /* CACHED */))
  ], 64 /* STABLE_FRAGMENT */))
}

Summary by CodeRabbit

  • New Features

    • Improved handling of static and empty interpolations, resulting in more efficient code generation and output for constant expressions.
    • Added new tests to verify correct stringification of static interpolation nodes and ensure output consistency.
  • Bug Fixes

    • Enhanced recognition of the string "undefined" as a literal value, improving constant folding and optimization.
  • Refactor

    • Centralized and reused the constant expression evaluation utility, reducing code duplication and improving maintainability.

Copy link

coderabbitai bot commented Jul 11, 2025

Walkthrough

The updates introduce optimizations for static interpolation handling in the compiler. New tests verify that constant and empty interpolations are stringified at compile time. The code generator now inlines constant interpolation values, and a utility for evaluating constant expressions is added and reused. Related stringification logic and tests are updated accordingly.

Changes

File(s) Change Summary
packages/compiler-core/tests/codegen.spec.ts
packages/compiler-dom/tests/transforms/stringifyStatic.spec.ts
Added tests for static and empty interpolation handling, ensuring correct code generation and stringification.
packages/compiler-core/src/codegen.ts Optimized genInterpolation to inline constant and empty interpolations as string literals.
packages/compiler-core/src/transforms/transformExpression.ts Improved literal recognition and ensured empty expressions are marked as stringifiable constants.
packages/compiler-core/src/utils.ts Added evaluateConstant utility for evaluating constant expressions as strings.
packages/compiler-dom/src/transforms/stringifyStatic.ts Replaced local evaluateConstant with import; improved interpolation handling and helper management.

Sequence Diagram(s)

sequenceDiagram
    participant Parser
    participant Transformer
    participant Codegen
    participant Utils

    Parser->>Transformer: Parse template with interpolation
    Transformer->>Transformer: Process expression, mark as constant if applicable
    Transformer->>Codegen: Pass InterpolationNode
    Codegen->>Utils: evaluateConstant(expression)
    Utils-->>Codegen: Stringified constant value
    Codegen->>Codegen: Inline string literal if constant
    Codegen-->>Output: Generated code with inlined value
Loading

Suggested labels

ready to merge, scope: compiler

Suggested reviewers

  • edison1105

Poem

In the warren where code bunnies hop,
Static strings now bubble to the top.
Interpolations, once runtime-bound,
Are stringified early—how sweet the sound!
With helpers trimmed and tests anew,
This patch brings clarity—thanks to the crew!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@@ -148,7 +148,7 @@ return function render(_ctx, _cache) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
Copy link
Author

Choose a reason for hiding this comment

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

We can't remove the toDisplayString here since the optimisation for this test is performed during codegen, after we've emitted the import.
This shouldn't matter since any unused imports will be tree-shaken by most build tools.

Comment on lines +771 to +775
if (node.content.content) {
push(JSON.stringify(toDisplayString(evaluateConstant(node.content))))
} else {
push(`""`)
}
Copy link
Author

Choose a reason for hiding this comment

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

We can't call evaluateConstant when node.content is undefined, so we just emit an empty string (which is the behaviour of `toDisplayString(undefined)"

@@ -44,7 +44,9 @@ import { parseExpression } from '@babel/parser'
import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'

const isLiteralWhitelisted = /*@__PURE__*/ makeMap('true,false,null,this')
const isLiteralWhitelisted = /*@__PURE__*/ makeMap(
'true,false,null,undefined,this',
Copy link
Author

Choose a reason for hiding this comment

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

undefined can be used as a variable name, but we check if it's a variable everywhere we use isLiteralWhitelisted, so I believe this change is sound.

// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transforms/transformExpression)
export function evaluateConstant(exp: ExpressionNode): string {
Copy link
Author

Choose a reason for hiding this comment

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

Moved - unchanged

@@ -304,6 +305,17 @@ function stringifyNode(
case NodeTypes.COMMENT:
return `<!--${escapeHtml(node.content)}-->`
case NodeTypes.INTERPOLATION:
// We add TO_DISPLAY_STRING for every interpolation, so we need to
// decrease its usage count whenever we remove an interpolation.
context.removeHelper(TO_DISPLAY_STRING)
Copy link
Author

Choose a reason for hiding this comment

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

This isn't strictly necessary (see above comment about unused imports), but doesn't hurt to cleanup.

node.content.type === NodeTypes.SIMPLE_EXPRESSION &&
!node.content.content
) {
return ''
Copy link
Author

Choose a reason for hiding this comment

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

Again, can't call evaluateConstant(undefined)

@CamWass CamWass marked this pull request as ready for review July 11, 2025 05:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 347ef1d and eced858.

⛔ Files ignored due to path filters (3)
  • packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap is excluded by !**/*.snap
  • packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap is excluded by !**/*.snap
  • packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • packages/compiler-core/__tests__/codegen.spec.ts (2 hunks)
  • packages/compiler-core/src/codegen.ts (4 hunks)
  • packages/compiler-core/src/transforms/transformExpression.ts (2 hunks)
  • packages/compiler-core/src/utils.ts (2 hunks)
  • packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts (1 hunks)
  • packages/compiler-dom/src/transforms/stringifyStatic.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
packages/compiler-core/__tests__/codegen.spec.ts (3)
packages/compiler-core/src/ast.ts (3)
  • InterpolationNode (249-252)
  • locStub (585-589)
  • createSimpleExpression (685-698)
packages/compiler-core/src/codegen.ts (1)
  • generate (284-407)
packages/compiler-core/src/index.ts (1)
  • generate (25-25)
packages/compiler-core/src/utils.ts (4)
packages/compiler-core/src/ast.ts (1)
  • ExpressionNode (91-91)
packages/shared/src/general.ts (2)
  • isString (51-51)
  • isSymbol (52-52)
packages/runtime-core/src/index.ts (1)
  • toDisplayString (377-377)
packages/shared/src/toDisplayString.ts (1)
  • toDisplayString (24-36)
packages/compiler-core/src/codegen.ts (3)
packages/runtime-core/src/index.ts (1)
  • toDisplayString (377-377)
packages/shared/src/toDisplayString.ts (1)
  • toDisplayString (24-36)
packages/compiler-core/src/utils.ts (1)
  • evaluateConstant (581-601)
packages/compiler-core/src/transforms/transformExpression.ts (1)
packages/shared/src/index.ts (1)
  • makeMap (1-1)
packages/compiler-dom/src/transforms/stringifyStatic.ts (1)
packages/compiler-core/src/runtimeHelpers.ts (1)
  • TO_DISPLAY_STRING (44-46)
🔇 Additional comments (12)
packages/compiler-core/__tests__/codegen.spec.ts (3)

6-6: LGTM! Import addition supports new test cases.

The InterpolationNode type import is correctly added to support the new test cases that directly construct interpolation nodes.


196-213: Excellent test coverage for static interpolation optimization.

The test effectively verifies that complex static expressions are evaluated at compile time and inlined as string literals. The test expression includes diverse literal types (strings, numbers, booleans, null, undefined, template literals) which provides comprehensive coverage of the optimization.

The expected output "hello1falseundefinednullhi" correctly represents the JavaScript string conversion behavior.


215-228: Good coverage for empty interpolation edge case.

This test ensures that empty interpolations are handled gracefully by outputting an empty string literal rather than generating a runtime helper call. This is an important optimization for templates with empty interpolations.

packages/compiler-core/src/codegen.ts (2)

10-10: Appropriate imports for the interpolation optimization.

The imports are correctly added to support the new optimization:

  • ConstantTypes for checking if expressions can be stringified
  • evaluateConstant for evaluating constant expressions at compile time
  • toDisplayString for consistent string conversion behavior

Also applies to: 36-36, 46-46


767-779: Well-implemented compile-time interpolation optimization.

The optimization correctly:

  1. Checks if the interpolation content is a simple expression marked as CAN_STRINGIFY
  2. Handles empty content by outputting an empty string literal
  3. Uses evaluateConstant and toDisplayString for consistent evaluation matching runtime behavior
  4. Falls back to the original runtime helper call for non-constant interpolations

The logic preserves the original behavior while adding the optimization, ensuring backward compatibility.

packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts (1)

529-561: Comprehensive test for static interpolation stringification.

This test effectively validates that static interpolations are stringified equivalently to their static text counterparts. The test design is excellent:

  1. Comprehensive coverage: Tests various literal types ("1", `1`, 1, false, undefined, null, empty string)
  2. Threshold calculation: Dynamically calculates repetitions needed to trigger stringification
  3. Equivalence verification: Ensures interpolated and static versions produce identical output
  4. Snapshot consistency: Maintains output stability

The test correctly maps interpolated expressions to their expected string representations, ensuring the optimization produces correct results.

packages/compiler-dom/src/transforms/stringifyStatic.ts (2)

15-15: Good refactoring to use centralized utilities.

The addition of TO_DISPLAY_STRING import and evaluateConstant import aligns with the broader refactoring to use centralized utilities across the compiler. This improves maintainability and consistency.

Also applies to: 20-20


308-318: Proper helper management and empty content handling.

The enhanced interpolation handling correctly:

  1. Helper accounting: Calls context.removeHelper(TO_DISPLAY_STRING) to decrement the helper usage count when an interpolation is removed during stringification
  2. Empty content guard: Returns empty string for simple expressions with empty content, preventing evaluateConstant from being called on undefined content
  3. Consistent evaluation: Uses the centralized evaluateConstant utility for reliable constant evaluation

The logic properly handles edge cases while maintaining the optimization's effectiveness.

packages/compiler-core/src/transforms/transformExpression.ts (2)

47-49: Appropriate expansion of literal whitelist.

Adding "undefined" to the literal whitelist is correct and enables the compiler to recognize undefined as a stringifiable constant. This supports the static interpolation optimization by allowing undefined expressions to be evaluated at compile time.

The change is consistent with JavaScript's treatment of undefined as a primitive value that can be safely stringified.


124-133: Well-structured early return logic for empty expressions.

The reorganization of early return conditions is well-designed:

  1. Empty content handling: First checks for empty content and marks it as CAN_STRINGIFY before returning
  2. Prefix identifier check: Then checks context.prefixIdentifiers and returns if false
  3. Optimization enablement: Ensures empty expressions can be optimized even when prefix identifiers is disabled

This change enables the static interpolation optimization to work correctly with empty expressions while maintaining the existing logic flow.

packages/compiler-core/src/utils.ts (2)

40-46: LGTM: Imports for new utility function.

The imports are correctly added to support the new evaluateConstant function implementation.


581-581: Inconsistent with past review comment.

The past review comment indicates this line was "Moved - unchanged", but this appears to be a new function addition rather than a move operation.

Likely an incorrect or invalid review comment.

Comment on lines +574 to +601
// __UNSAFE__
// Reason: eval.
// It's technically safe to eval because only constant expressions are possible
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transforms/transformExpression)
export function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return (${exp.content})`)()
} else {
// compound
let res = ``
exp.children.forEach(c => {
if (isString(c) || isSymbol(c)) {
return
}
if (c.type === NodeTypes.TEXT) {
res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
res += evaluateConstant(c as ExpressionNode)
}
})
return res
}
}
Copy link

@coderabbitai coderabbitai bot Jul 11, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and improve type safety.

The function implementation looks good overall but has a few areas for improvement:

  1. Missing error handling: The new Function() call should be wrapped in try-catch to handle malformed expressions gracefully.
  2. Unsafe type casting: Line 596 uses c as ExpressionNode without type checking, which could lead to runtime errors.

Apply this diff to improve error handling and type safety:

 export function evaluateConstant(exp: ExpressionNode): string {
   if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
-    return new Function(`return (${exp.content})`)()
+    try {
+      return new Function(`return (${exp.content})`)()
+    } catch (e) {
+      // Fallback for malformed expressions
+      return exp.content
+    }
   } else {
     // compound
     let res = ``
     exp.children.forEach(c => {
       if (isString(c) || isSymbol(c)) {
         return
       }
       if (c.type === NodeTypes.TEXT) {
         res += c.content
       } else if (c.type === NodeTypes.INTERPOLATION) {
         res += toDisplayString(evaluateConstant(c.content))
       } else {
-        res += evaluateConstant(c as ExpressionNode)
+        if (isObject(c) && 'type' in c) {
+          res += evaluateConstant(c as ExpressionNode)
+        }
       }
     })
     return res
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// __UNSAFE__
// Reason: eval.
// It's technically safe to eval because only constant expressions are possible
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transforms/transformExpression)
export function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return (${exp.content})`)()
} else {
// compound
let res = ``
exp.children.forEach(c => {
if (isString(c) || isSymbol(c)) {
return
}
if (c.type === NodeTypes.TEXT) {
res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
res += evaluateConstant(c as ExpressionNode)
}
})
return res
}
}
// __UNSAFE__
// Reason: eval.
// It's technically safe to eval because only constant expressions are possible
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transforms/transformExpression)
export function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
try {
return new Function(`return (${exp.content})`)()
} catch (e) {
// Fallback for malformed expressions
return exp.content
}
} else {
// compound
let res = ``
exp.children.forEach(c => {
if (isString(c) || isSymbol(c)) {
return
}
if (c.type === NodeTypes.TEXT) {
res += c.content
} else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content))
} else {
if (isObject(c) && 'type' in c) {
res += evaluateConstant(c as ExpressionNode)
}
}
})
return res
}
}
🤖 Prompt for AI Agents
In packages/compiler-core/src/utils.ts between lines 574 and 601, the
evaluateConstant function lacks error handling around the new Function call and
uses unsafe type casting on line 596. To fix this, wrap the new Function
invocation in a try-catch block to gracefully handle any errors from malformed
expressions, returning a safe fallback value or rethrowing as needed.
Additionally, before casting c to ExpressionNode on line 596, add a type check
to ensure c is indeed an ExpressionNode to prevent runtime errors.

Copy link
Author

Choose a reason for hiding this comment

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

I feel like this util was written in a specific way, for it's specific use-case, so changing it is out-of-scope, but happy to look into it if the human reviewer would like me to

Copy link

Choose a reason for hiding this comment

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

@CamWass That's a perfectly reasonable approach! You're right that this function was designed for its specific use case within the compiler optimization, and the current implementation aligns with that purpose. The safety measures you've mentioned in the comments (constant expressions only, no parens) do provide reasonable protection for the intended use case.

If a human reviewer feels the error handling should be added, we can always revisit it. For now, keeping the focus on the PR's core objective of compile-time static interpolation evaluation makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant