Skip to content
Open
2 changes: 1 addition & 1 deletion src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ impl Number {
Self::to_string_from_f64(self.value(), bump)
}

pub(crate) fn to_string_from_f64(value: f64, bump: &Bump) -> Option<Str> {
pub fn to_string_from_f64(value: f64, bump: &Bump) -> Option<Str> {
if value == value.trunc() && (value < i32::MAX as f64 && value > i32::MIN as f64) {
let int_value = value as i64;
let abs = int_value.unsigned_abs();
Expand Down
56 changes: 47 additions & 9 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ pub struct RecentlyVisitedTSNamespace {
pub(crate) map: Option<js_ast::StoreRef<js_ast::TSNamespaceMemberMap>>,
}

/// The value of a TypeScript constant expression. The string is never a rope.
#[derive(Clone, Copy)]
pub(crate) enum TSConstantValue {
Number(f64),
String(js_ast::StoreRef<E::EString>),
}

#[derive(Clone, Copy)]
pub struct ReactRefreshImportClause<'a> {
pub(crate) name: &'a [u8],
Expand Down Expand Up @@ -694,6 +701,12 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
pub(crate) ts_namespace: RecentlyVisitedTSNamespace,
pub(crate) top_level_enums: List<'a, Ref>,

/// `const` values an enum initializer can fold (TS 5.0), see `record_ts_enum_constants`.
pub(crate) ts_enum_constants: HashMap<Ref, TSConstantValue>,
pub(crate) is_visiting_ts_enum_initializer: bool,
/// `const x: T = ...`: tsc does not fold a const with a type annotation.
pub(crate) ts_annotated_constants: RefMap,

// Value is a shared `&'a [ScopeOrder<'a>]`. The visit pass never writes
// through these slices — it only reads
// `Copy` elements and advances a cursor — so the map and
Expand Down Expand Up @@ -2270,6 +2283,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if TYPESCRIPT {
if self.is_visiting_ts_enum_initializer
&& let Some(&value) = self.ts_enum_constants.get(&ref_)
{
self.ignore_usage(ref_);
let name = self.symbols[ref_.inner_index() as usize]
.original_name
.slice();
let value = match value {
TSConstantValue::Number(num) => Expr {
loc,
data: js_ast::ExprData::ENumber(E::Number::new(num)),
},
TSConstantValue::String(str_) => self.new_expr(&*str_, loc),
};
return self.wrap_inlined_enum(value, name);
}

if let Some(member_data) = self.ref_to_ts_namespace_member.get(&ref_) {
match member_data {
js_ast::ts::Data::EnumNumber(num) => {
Expand Down Expand Up @@ -7245,6 +7275,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

/// Drops the entries keyed by symbols at index `symbols_len` and above.
fn forget_symbols_from<V>(map: &mut HashMap<Ref, V>, symbols_len: usize) {
let stale: Vec<Ref> = map
.keys()
.filter(|ref_| ref_.inner_index() as usize >= symbols_len)
.copied()
.collect();
for ref_ in stale {
map.remove(&ref_);
}
}

/// The unscoped npm package of a specifier (`react/x`) or path (`node_modules<sep>react<sep>x.js`).
fn path_package_name<'a>(path: &fs::Path<'a>) -> Option<&'a [u8]> {
let (name_to_use, separators): (&[u8], &[u8]) =
Expand Down Expand Up @@ -8329,15 +8371,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if TYPESCRIPT {
self.ts_use_counts.truncate(snapshot.symbols_len);
}
let stale: Vec<Ref> = self
.ref_to_ts_namespace_member
.keys()
.filter(|ref_| ref_.inner_index() as usize >= snapshot.symbols_len)
.copied()
.collect();
for ref_ in stale {
self.ref_to_ts_namespace_member.remove(&ref_);
}
forget_symbols_from(&mut self.ref_to_ts_namespace_member, snapshot.symbols_len);
forget_symbols_from(&mut self.ts_annotated_constants, snapshot.symbols_len);
}
self.allocated_names.truncate(snapshot.allocated_names_len);
self.import_records.truncate(snapshot.import_records_len);
Expand Down Expand Up @@ -9827,6 +9862,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
map: None,
},
top_level_enums: BumpVec::new_in(arena),
ts_enum_constants: Default::default(),
is_visiting_ts_enum_initializer: false,
ts_annotated_constants: Default::default(),
scopes_in_order_for_enum: Default::default(),
will_wrap_module_in_try_catch_for_using: false,
nearest_stmt_list: None,
Expand Down
5 changes: 5 additions & 0 deletions src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if is_definite_assignment_assertion || p.lexer.token == T::TColon {
p.lexer.expect(T::TColon)?;
p.skip_type_script_type(Level::Lowest)?;
if kind == js_ast::symbol::Kind::Constant
&& let js_ast::binding::Data::BIdentifier(id) = local.data
{
p.ts_annotated_constants.insert(id.r#ref, ());
}
}
}

Expand Down
36 changes: 20 additions & 16 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,24 +991,28 @@ impl<'a> Parser<'a> {
let mut preprocessed_enum_i: usize = 0;
if p.scopes_in_order_for_enum.count() > 0 {
for stmt in stmts.iter_mut() {
if matches!(stmt.data, js_ast::StmtData::SEnum(_)) {
let old_scopes_in_order = p.scope_order_to_visit;
let idx = p
.scopes_in_order_for_enum
.keys()
.iter()
.position(|k| *k == stmt.loc)
.expect("enum scope-order entry recorded during parse");
// Map stores `&'a [ScopeOrder]`; shared borrow may freely alias the inner
// re-lookup performed by `append_part → visit_stmts`.
p.scope_order_to_visit = p.scopes_in_order_for_enum.values()[idx];
match stmt.data {
js_ast::StmtData::SEnum(_) => {
let old_scopes_in_order = p.scope_order_to_visit;
let idx = p
.scopes_in_order_for_enum
.keys()
.iter()
.position(|k| *k == stmt.loc)
.expect("enum scope-order entry recorded during parse");
// A shared `&'a [ScopeOrder]`: `append_part -> visit_stmts` may re-read it.
p.scope_order_to_visit = p.scopes_in_order_for_enum.values()[idx];

let mut enum_parts = BumpVec::<js_ast::Part>::new_in(arena);
let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut enum_parts, sliced)?;
preprocessed_enums.push(enum_parts);
let mut enum_parts = BumpVec::<js_ast::Part>::new_in(arena);
let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut enum_parts, sliced)?;
preprocessed_enums.push(enum_parts);

p.scope_order_to_visit = old_scopes_in_order;
p.scope_order_to_visit = old_scopes_in_order;
}
// An enum above can only see the constants declared before it.
js_ast::StmtData::SLocal(local) => p.record_ts_enum_constants(&local),
_ => {}
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,41 @@ pub(crate) fn float_to_int32(f: f64) -> i32 {
if f < 0.0 { 0i32.wrapping_sub(int) } else { int }
}

/// A JavaScript arithmetic or bitwise binary operator on two numbers, `None` for other operators.
pub(crate) fn fold_numeric_binary_operator(
op: js_ast::OpCode,
left: f64,
right: f64,
) -> Option<f64> {
use js_ast::OpCode as Op;
Some(match op {
Op::BinAdd => left + right,
Op::BinSub => left - right,
Op::BinMul => left * right,
Op::BinDiv => left / right,
// f64 `%` is fmod (LLVM frem), which is what JavaScriptCore does (`Math::fmodDouble`).
Op::BinRem => left % right,
Op::BinPow => js_ast::math::pow(left, right),
Op::BinShl => {
let right: u32 = (float_to_int32(right) as u32) % 32;
float_to_int32(left).wrapping_shl(right) as f64
}
Op::BinShr => {
let right: u32 = (float_to_int32(right) as u32) % 32;
// wrapping_shr on i32 is an arithmetic shift right
float_to_int32(left).wrapping_shr(right) as f64
}
Op::BinUShr => {
let right: u32 = (float_to_int32(right) as u32) % 32;
(float_to_int32(left) as u32).wrapping_shr(right) as f64
}
Op::BinBitwiseAnd => (float_to_int32(left) & float_to_int32(right)) as f64,
Op::BinBitwiseOr => (float_to_int32(left) | float_to_int32(right)) as f64,
Op::BinBitwiseXor => (float_to_int32(left) ^ float_to_int32(right)) as f64,
_ => return None,
})
}

#[derive(Clone, Copy, Default)]
pub struct ParseBindingOptions {
/// This will prevent parsing of destructuring patterns, as using statement
Expand Down
30 changes: 17 additions & 13 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1520,19 +1520,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut preprocessed_enums: ListManaged<'a, &'a [Stmt]> = ListManaged::new_in(p.arena);
if p.scopes_in_order_for_enum.count() > 0 {
for stmt in stmts.iter_mut() {
if matches!(stmt.data, StmtData::SEnum(_)) {
// `scope_order_to_visit: &'a [ScopeOrder<'a>]` is `Copy`;
// plain save/restore.
let old_scopes_in_order = p.scope_order_to_visit;

p.scope_order_to_visit =
scopes_for_enum_at(&p.scopes_in_order_for_enum, stmt.loc);

let mut temp = ListManaged::new_in(p.arena);
let res = p.visit_and_append_stmt(&mut temp, stmt);
p.scope_order_to_visit = old_scopes_in_order;
res?;
preprocessed_enums.push(temp.into_bump_slice());
match stmt.data {
StmtData::SEnum(_) => {
// `scope_order_to_visit` is a `Copy` slice: plain save/restore.
let old_scopes_in_order = p.scope_order_to_visit;

p.scope_order_to_visit =
scopes_for_enum_at(&p.scopes_in_order_for_enum, stmt.loc);

let mut temp = ListManaged::new_in(p.arena);
let res = p.visit_and_append_stmt(&mut temp, stmt);
p.scope_order_to_visit = old_scopes_in_order;
res?;
preprocessed_enums.push(temp.into_bump_slice());
}
// An enum above can only see the constants declared before it.
StmtData::SLocal(local) => p.record_ts_enum_constants(&local),
_ => {}
}
}
}
Expand Down
132 changes: 16 additions & 116 deletions src/js_parser/visit/visit_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bun_collections::VecExt;
use core::cmp::Ordering;

use crate::p::P;
use crate::parser::{ExprIn, float_to_int32, prefill};
use crate::parser::{ExprIn, fold_numeric_binary_operator, prefill};
use crate::scan::scan_side_effects::SideEffects;
use bun_ast::fold_string_addition::{FoldStringAdditionKind, fold_string_addition};
use bun_ast::{
Expand Down Expand Up @@ -456,123 +456,23 @@ impl BinaryExpressionVisitor {
}
}
}
Op::Code::BinSub => {
Op::Code::BinSub
| Op::Code::BinMul
| Op::Code::BinDiv
| Op::Code::BinRem
| Op::Code::BinPow
| Op::Code::BinShl
| Op::Code::BinShr
| Op::Code::BinUShr
| Op::Code::BinBitwiseAnd
| Op::Code::BinBitwiseOr
| Op::Code::BinBitwiseXor => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(E::Number::new(vals[0] - vals[1]), v.loc);
}
}
}
Op::Code::BinMul => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(E::Number::new(vals[0] * vals[1]), v.loc);
}
}
}
Op::Code::BinDiv => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(E::Number::new(vals[0] / vals[1]), v.loc);
}
}
}
Op::Code::BinRem => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(
// Rust `%` on f64 has libc fmod semantics (LLVM frem),
// which matches what JavaScriptCore does:
// https://github.com/oven-sh/WebKit/blob/7a0b13626e5db69aa5a32d037431d381df5dfb61/Source/JavaScriptCore/runtime/MathCommon.cpp#L574-L597
E::Number::new(vals[0] % vals[1]),
v.loc,
);
}
}
}
Op::Code::BinPow => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p
.new_expr(E::Number::new(bun_ast::math::pow(vals[0], vals[1])), v.loc);
}
}
}
Op::Code::BinShl => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
let left = float_to_int32(vals[0]);
let right: u32 = (float_to_int32(vals[1]) as u32) % 32;
let result: i32 = left.wrapping_shl(right);
return p.new_expr(E::Number::new(result as f64), v.loc);
}
}
}
Op::Code::BinShr => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
let left = float_to_int32(vals[0]);
let right: u32 = (float_to_int32(vals[1]) as u32) % 32;
// wrapping_shr on i32 is an arithmetic shift right
let result: i32 = left.wrapping_shr(right);
return p.new_expr(E::Number::new(result as f64), v.loc);
}
}
}
Op::Code::BinUShr => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
let left: u32 = float_to_int32(vals[0]) as u32;
let right: u32 = (float_to_int32(vals[1]) as u32) % 32;
let result: u32 = left.wrapping_shr(right);
return p.new_expr(E::Number::new(result as f64), v.loc);
}
}
}
Op::Code::BinBitwiseAnd => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(
E::Number::new(
(float_to_int32(vals[0]) & float_to_int32(vals[1])) as f64,
),
v.loc,
);
}
}
}
Op::Code::BinBitwiseOr => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
if let Some([left, right]) =
Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
&& let Some(result) = fold_numeric_binary_operator(e_.op, left, right)
{
return p.new_expr(
E::Number::new(
(float_to_int32(vals[0]) | float_to_int32(vals[1])) as f64,
),
v.loc,
);
}
}
}
Op::Code::BinBitwiseXor => {
if p.should_fold_typescript_constant_expressions {
if let Some(vals) = Expr::extract_numeric_values(&e_.left.data, &e_.right.data)
{
return p.new_expr(
E::Number::new(
(float_to_int32(vals[0]) ^ float_to_int32(vals[1])) as f64,
),
v.loc,
);
return p.new_expr(E::Number::new(result), v.loc);
}
}
}
Expand Down
Loading
Loading