mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(ecmascript): resolve identifier value type via tracked constants (#22234)
`value_type` for `Expression::Identifier` returned `Undetermined` for any non-global identifier, **even when the binding had zero writes and a known constant initializer**. This causes code like `slot?.()` on a `let slot;` with all writers tree-shaken away not being folded to `void 0`, leaving dead code in the output. This PR changes that by checking `GlobalContext::get_constant_value_for_reference_id` for non-global identifier references and mapping the resulting `ConstantValue` to a `ValueType` via a new private helper. Once the identifier resolves to a tracked nullish value, the existing nullish branch in `fold_chain_expr` collapses the chain to `void 0`. The same lookup also works for the opposite case (#21923). The lookup is gated by making sure there are no write references, so anything that is being written anywhere keeps its `Undetermined` type. Closes rolldown/rolldown#9281 Related (but does not resolve) #21923 --------- Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
1 parent
602dfd6294
commit
0f26de6dd2
7 files changed
+99
-13
No files matched your search
@@ -3,6 +3,7 @@ use std::borrow::Cow;
|
||||
use num_bigint::BigInt;
|
||||
use num_traits::Zero;
|
||||
|
||||
use super::value_type::ValueType;
|
||||
use crate::{GlobalContext, ToBoolean, ToJsString, ToNumber};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@@ -40,6 +41,17 @@ impl<'a> ConstantValue<'a> {
|
||||
matches!(self, Self::Null)
|
||||
}
|
||||
|
||||
pub fn value_type(&self) -> ValueType {
|
||||
match self {
|
||||
Self::Number(_) => ValueType::Number,
|
||||
Self::BigInt(_) => ValueType::BigInt,
|
||||
Self::String(_) => ValueType::String,
|
||||
Self::Boolean(_) => ValueType::Boolean,
|
||||
Self::Undefined => ValueType::Undefined,
|
||||
Self::Null => ValueType::Null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Option<Cow<'a, str>> {
|
||||
match self {
|
||||
Self::String(s) => Some(s),
|
||||
|
||||
@@ -92,7 +92,11 @@ impl<'a> DetermineValueType<'a> for Expression<'a> {
|
||||
_ => ValueType::Undetermined,
|
||||
}
|
||||
} else {
|
||||
ValueType::Undetermined
|
||||
ident
|
||||
.reference_id
|
||||
.get()
|
||||
.and_then(|reference_id| ctx.value_type_for_reference_id(reference_id))
|
||||
.unwrap_or(ValueType::Undetermined)
|
||||
}
|
||||
}
|
||||
Expression::UnaryExpression(e) => e.value_type(ctx),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use oxc_ast::ast::{Expression, IdentifierReference};
|
||||
use oxc_syntax::reference::ReferenceId;
|
||||
|
||||
use crate::constant_evaluation::ConstantValue;
|
||||
use crate::constant_evaluation::{ConstantValue, ValueType};
|
||||
|
||||
pub trait GlobalContext<'a>: Sized {
|
||||
/// Whether the reference is a global reference.
|
||||
@@ -19,6 +19,15 @@ pub trait GlobalContext<'a>: Sized {
|
||||
) -> Option<ConstantValue<'a>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The [`ValueType`] of the constant a reference resolves to, if known.
|
||||
///
|
||||
/// Cheaper than [`Self::get_constant_value_for_reference_id`] when only the
|
||||
/// type discriminant is needed, since it avoids cloning the value
|
||||
/// (relevant for `BigInt` / owned `String`).
|
||||
fn value_type_for_reference_id(&self, _reference_id: ReferenceId) -> Option<ValueType> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WithoutGlobalReferenceInformation;
|
||||
|
||||
@@ -3,7 +3,8 @@ use oxc_compat::ESFeature;
|
||||
use oxc_ecmascript::{
|
||||
GlobalContext,
|
||||
constant_evaluation::{
|
||||
ConstantEvaluation, ConstantEvaluationCtx, ConstantValue, binary_operation_evaluate_value,
|
||||
ConstantEvaluation, ConstantEvaluationCtx, ConstantValue, ValueType,
|
||||
binary_operation_evaluate_value,
|
||||
},
|
||||
side_effects::{
|
||||
MayHaveSideEffects, MayHaveSideEffectsContext, PropertyReadSideEffects, is_pure_function,
|
||||
@@ -33,13 +34,11 @@ impl<'a> GlobalContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
|
||||
&self,
|
||||
reference_id: ReferenceId,
|
||||
) -> Option<ConstantValue<'a>> {
|
||||
self.scoping()
|
||||
.get_reference(reference_id)
|
||||
.symbol_id()
|
||||
.and_then(|symbol_id| self.state.symbol_values.get_symbol_value(symbol_id))
|
||||
.filter(|sv| sv.write_references_count == 0)
|
||||
.and_then(|sv| sv.initialized_constant.as_ref())
|
||||
.cloned()
|
||||
self.tracked_constant_for_reference_id(reference_id).cloned()
|
||||
}
|
||||
|
||||
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
|
||||
self.tracked_constant_for_reference_id(reference_id).map(ConstantValue::value_type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +53,10 @@ impl<'a> GlobalContext<'a> for &TraverseCtx<'a, MinifierState<'a>> {
|
||||
) -> Option<ConstantValue<'a>> {
|
||||
(*self).get_constant_value_for_reference_id(reference_id)
|
||||
}
|
||||
|
||||
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
|
||||
(*self).value_type_for_reference_id(reference_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GlobalContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
|
||||
@@ -67,6 +70,10 @@ impl<'a> GlobalContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
|
||||
) -> Option<ConstantValue<'a>> {
|
||||
(**self).get_constant_value_for_reference_id(reference_id)
|
||||
}
|
||||
|
||||
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
|
||||
(**self).value_type_for_reference_id(reference_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MayHaveSideEffectsContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
|
||||
@@ -177,6 +184,18 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
ident.is_global_reference(self.scoping())
|
||||
}
|
||||
|
||||
fn tracked_constant_for_reference_id(
|
||||
&self,
|
||||
reference_id: ReferenceId,
|
||||
) -> Option<&ConstantValue<'a>> {
|
||||
self.scoping()
|
||||
.get_reference(reference_id)
|
||||
.symbol_id()
|
||||
.and_then(|symbol_id| self.state.symbol_values.get_symbol_value(symbol_id))
|
||||
.filter(|sv| sv.write_references_count == 0)
|
||||
.and_then(|sv| sv.initialized_constant.as_ref())
|
||||
}
|
||||
|
||||
pub fn eval_binary(&self, e: &BinaryExpression<'a>) -> Option<Expression<'a>> {
|
||||
if e.may_have_side_effects(self) {
|
||||
None
|
||||
|
||||
@@ -589,3 +589,45 @@ fn drop_optional_chain_on_non_nullish_base() {
|
||||
test("export const v = null?.foo.bar", "export const v = void 0");
|
||||
test("export const v = []?.foo.bar", "export const v = [].foo.bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_optional_chain_on_undefined_let_binding() {
|
||||
// https://github.com/rolldown/rolldown/issues/9281
|
||||
// A `let` binding with no writes is statically known to be `undefined`,
|
||||
// so optional calls / member accesses on it should fold to `void 0`.
|
||||
test("let slot; export function call() { slot?.() }", "export function call() {}");
|
||||
test("let slot; export function call() { slot?.foo }", "export function call() {}");
|
||||
test("let slot; export function call() { slot?.[foo()] }", "export function call() {}");
|
||||
// A binding that is written somewhere is not nullish-known: leave it alone.
|
||||
test_same(
|
||||
"let slot; export function setSlot(v) { slot = v } export function call() { slot?.() }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_optional_chain_on_null_const_binding() {
|
||||
// A `const` initialized to `null` resolves to `ValueType::Null`, so the
|
||||
// optional chain folds the same way the `undefined` case does.
|
||||
test("const slot = null; export function call() { slot?.() }", "export function call() {}");
|
||||
test("const slot = null; export function call() { slot?.foo }", "export function call() {}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_coalesce_on_tracked_non_nullish_binding() {
|
||||
// The new value_type lookup also resolves non-nullish constants, so the
|
||||
// right-hand side of `??` can be dropped.
|
||||
//
|
||||
// Two reads + a string the inliner skips (length > 3) prevents
|
||||
// `inline_identifier_reference` from short-circuiting the test by
|
||||
// substituting the literal value before the coalesce fold runs.
|
||||
test(
|
||||
"let s = 'hello'; export function a() { return s ?? other() } export function b() { return s ?? other() }",
|
||||
"let s = 'hello'; export function a() { return s } export function b() { return s }",
|
||||
);
|
||||
// BigInt is never inlined, so a single read is enough to exercise the
|
||||
// value-type path here.
|
||||
test(
|
||||
"let n = 5n; export function a() { return n ?? other() } export function b() { return n ?? other() }",
|
||||
"let n = 5n; export function a() { return n } export function b() { return n }",
|
||||
);
|
||||
}
|
||||
@@ -11,9 +11,9 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
544.10 kB | 71.04 kB | 72.48 kB | 25.77 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 267.41 kB | 270.13 kB | 88.02 kB | 90.80 kB | 2 | d3.js
|
||||
555.77 kB | 267.40 kB | 270.13 kB | 88.00 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 439.37 kB | 458.89 kB | 122.05 kB | 126.71 kB | 2 | bundle.min.js
|
||||
1.01 MB | 439.35 kB | 458.89 kB | 122.05 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 642.65 kB | 646.76 kB | 159.41 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ cal.com.tsx | 1.06 MB || 16287 | 46 |
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 24 | 6 || 31 | 6
|
||||
|
||||
pdf.mjs | 567.30 kB || 4016 | 417 || 47474 | 7746
|
||||
pdf.mjs | 567.30 kB || 4016 | 417 || 47465 | 7740
|
||||
|
||||
antd.js | 6.69 MB || 7624 | 1657 || 333609 | 69349
|
||||
|
||||
|
||||
Reference in new issue
Block a user