diff --git a/src/ast/e.rs b/src/ast/e.rs index 7ae43d52aae1..72b63f677bf4 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -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 { + pub fn to_string_from_f64(value: f64, bump: &Bump) -> Option { 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(); diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 7aba6f7d8d38..6d1445b50e31 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -213,6 +213,13 @@ pub struct RecentlyVisitedTSNamespace { pub(crate) map: Option>, } +/// 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), +} + #[derive(Clone, Copy)] pub struct ReactRefreshImportClause<'a> { pub(crate) name: &'a [u8], @@ -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, + 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 @@ -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) => { @@ -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(map: &mut HashMap, symbols_len: usize) { + let stale: Vec = 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_modulesreactx.js`). fn path_package_name<'a>(path: &fs::Path<'a>) -> Option<&'a [u8]> { let (name_to_use, separators): (&[u8], &[u8]) = @@ -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 = 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); @@ -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, diff --git a/src/js_parser/parse/mod.rs b/src/js_parser/parse/mod.rs index e31557ff2656..b0196c14dccb 100644 --- a/src/js_parser/parse/mod.rs +++ b/src/js_parser/parse/mod.rs @@ -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, ()); + } } } diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 743cc7c322c6..9a493b79fc17 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -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::::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::::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), + _ => {} } } } diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 1911d609651d..f1dbd6736ac0 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -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 { + 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 diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index 0c9a78c8ad5f..a0572137e51e 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -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), + _ => {} } } } diff --git a/src/js_parser/visit/visit_binary.rs b/src/js_parser/visit/visit_binary.rs index 5c1ebed71e9d..47e9e07a8788 100644 --- a/src/js_parser/visit/visit_binary.rs +++ b/src/js_parser/visit/visit_binary.rs @@ -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::{ @@ -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); } } } diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs index 5de468c08a2e..6351597fb1e0 100644 --- a/src/js_parser/visit/visit_stmt.rs +++ b/src/js_parser/visit/visit_stmt.rs @@ -1,10 +1,10 @@ #![warn(unused_must_use)] use crate::Error; use crate::lexer as js_lexer; -use crate::p::{P, ReactRefreshExportKind}; +use crate::p::{P, ReactRefreshExportKind, TSConstantValue}; use crate::parser::{ PrependTempRefsOpts, ReactRefresh, Ref, RelocateVarsMode, SideEffects, StmtsKind, - statement_cares_about_scope, + float_to_int32, fold_numeric_binary_operator, statement_cares_about_scope, }; use bun_alloc::{ArenaVec as BumpVec, ArenaVecExt as _}; use bun_ast::ast_result::CommonJSExportValue; @@ -35,6 +35,118 @@ fn list_to_stmts<'a>(list: StmtList<'a>) -> StmtNodeList { StmtNodeList::from_bump(list) } +// ─── `P::eval_ts_constant_expression` helpers: free functions, so not compiled per `P` instantiation ─── + +fn ts_constant_string(str_: js_ast::StoreRef) -> TSConstantValue { + debug_assert!( + str_.next.is_none(), + "each substitution copies this node, and copies of a rope would share its tail" + ); + TSConstantValue::String(str_) +} + +fn new_ts_constant_string(bytes: &[u8]) -> TSConstantValue { + let expr = Expr::init(E::EString::init(bytes), bun_ast::Loc::EMPTY); + ts_constant_string(expr.data.e_string().expect("infallible: just created")) +} + +fn ts_namespace_member_value(data: js_ast::ts::Data) -> Option { + match data { + js_ast::ts::Data::EnumNumber(num) => Some(TSConstantValue::Number(num)), + // `s_enum` stores string members flat. + js_ast::ts::Data::EnumString(str_) => Some(ts_constant_string(str_)), + _ => None, + } +} + +/// An unbound `Infinity` or `NaN`, which tsc folds too. +fn ts_global_constant(name: &[u8]) -> Option { + match name { + b"Infinity" => Some(TSConstantValue::Number(f64::INFINITY)), + b"NaN" => Some(TSConstantValue::Number(f64::NAN)), + _ => None, + } +} + +fn fold_ts_constant_unary(op: js_ast::OpCode, value: TSConstantValue) -> Option { + let TSConstantValue::Number(value) = value else { + return None; + }; + match op { + js_ast::OpCode::UnPos => Some(TSConstantValue::Number(value)), + js_ast::OpCode::UnNeg => Some(TSConstantValue::Number(-value)), + js_ast::OpCode::UnCpl => Some(TSConstantValue::Number(f64::from(!float_to_int32(value)))), + _ => None, + } +} + +fn fold_ts_constant_binary( + op: js_ast::OpCode, + left: TSConstantValue, + right: TSConstantValue, + arena: &bun_alloc::Arena, +) -> Option { + match (left, right) { + (TSConstantValue::Number(left), TSConstantValue::Number(right)) => { + fold_numeric_binary_operator(op, left, right).map(TSConstantValue::Number) + } + _ if op == js_ast::OpCode::BinAdd => { + let mut bytes: BumpVec<'_, u8> = BumpVec::new_in(arena); + append_ts_constant_value(&mut bytes, left, arena)?; + append_ts_constant_value(&mut bytes, right, arena)?; + Some(new_ts_constant_string(bytes.into_bump_slice())) + } + _ => None, + } +} + +/// An untagged template whose parts evaluated to `values`. +fn fold_ts_constant_template( + template: &E::Template, + values: &[TSConstantValue], + arena: &bun_alloc::Arena, +) -> Option { + let E::TemplateContents::Cooked(head) = &template.head else { + return None; + }; + if !head.is_utf8() { + return None; + } + let mut bytes: BumpVec<'_, u8> = BumpVec::new_in(arena); + bytes.extend_from_slice(head.slice8()); + for (part, value) in template.parts().iter().zip(values) { + append_ts_constant_value(&mut bytes, *value, arena)?; + let E::TemplateContents::Cooked(tail) = &part.tail else { + return None; + }; + if !tail.is_utf8() { + return None; + } + bytes.extend_from_slice(tail.slice8()); + } + Some(new_ts_constant_string(bytes.into_bump_slice())) +} + +/// JavaScript string concatenation onto `bytes`. 8-bit strings only, like the parser's other folds. +fn append_ts_constant_value( + bytes: &mut BumpVec<'_, u8>, + value: TSConstantValue, + arena: &bun_alloc::Arena, +) -> Option<()> { + match value { + TSConstantValue::String(str_) => { + if !str_.is_utf8() { + return None; + } + bytes.extend_from_slice(str_.slice8()); + } + TSConstantValue::Number(num) => { + bytes.extend_from_slice(E::Number::to_string_from_f64(num, arena)?.slice()); + } + } + Some(()) +} + // a direct `impl P` block. The 30+ per-variant `s_*` helpers are private; only // `visit_and_append_stmt` is surfaced. Full draft body preserved under mod _draft below. @@ -2181,6 +2293,150 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O Ok(()) } + /// Records the `const`s of the unvisited `local` whose initializers are constant expressions. + pub(crate) fn record_ts_enum_constants(&mut self, local: &S::Local) { + if local.kind != S::Kind::KConst { + return; + } + for decl in local.decls.slice() { + let (js_ast::binding::Data::BIdentifier(id), Some(value)) = + (decl.binding.data, decl.value) + else { + continue; + }; + // With tree shaking a top-level declaration comes through twice (module, own part). + if self.ts_enum_constants.contains_key(&id.r#ref) + || self.ts_annotated_constants.contains_key(&id.r#ref) + { + continue; + } + if let Some(value) = self.eval_ts_constant_expression(&value) { + self.ts_enum_constants.insert(id.r#ref, value); + } + } + } + + /// tsc's `evaluate` for enum member initializers (microsoft/TypeScript#50528), on an unvisited expression. + fn eval_ts_constant_expression(&mut self, expr: &Expr) -> Option { + if !self.stack_check.is_safe_to_recurse() { + return None; + } + match expr.data { + js_ast::ExprData::ENumber(num) => Some(TSConstantValue::Number(num.value())), + // The parse pass builds no ropes. + js_ast::ExprData::EString(str_) => Some(ts_constant_string(str_)), + js_ast::ExprData::EUnary(unary) => { + let value = self.eval_ts_constant_expression(&unary.value)?; + fold_ts_constant_unary(unary.op, value) + } + js_ast::ExprData::EBinary(binary) => { + let left = self.eval_ts_constant_expression(&binary.left)?; + let right = self.eval_ts_constant_expression(&binary.right)?; + fold_ts_constant_binary(binary.op, left, right, self.arena) + } + js_ast::ExprData::ETemplate(template) => { + if template.tag.is_some() { + return None; + } + let mut values: smallvec::SmallVec<[TSConstantValue; 4]> = + smallvec::SmallVec::new(); + for part in template.parts() { + values.push(self.eval_ts_constant_expression(&part.value)?); + } + fold_ts_constant_template(&template, &values, self.arena) + } + js_ast::ExprData::EIdentifier(ident) => { + let name = self.load_name_from_ref(ident.ref_); + // Where "arguments" is forbidden the lookup logs an error: leave that to the visit. + if name == b"arguments" { + return None; + } + let result = self + .find_symbol_with_record_usage::(expr.loc, name) + .ok()?; + if result.is_inside_with_scope { + return None; + } + if result.r#ref.is_empty() + || self.symbols[result.r#ref.inner_index() as usize].kind + == js_ast::symbol::Kind::Unbound + { + return ts_global_constant(name); + } + if let Some(&value) = self.ts_enum_constants.get(&result.r#ref) { + return Some(value); + } + ts_namespace_member_value(*self.ref_to_ts_namespace_member.get(&result.r#ref)?) + } + js_ast::ExprData::EDot(dot) => { + if dot.optional_chain.is_some() { + return None; + } + let map = self.eval_ts_namespace_chain(&dot.target)?; + ts_namespace_member_value((*map).get(dot.name.slice())?.data) + } + js_ast::ExprData::EIndex(index) => { + if index.optional_chain.is_some() { + return None; + } + let js_ast::ExprData::EString(name) = index.index.data else { + return None; + }; + if !name.is_utf8() || name.next.is_some() { + return None; + } + let map = self.eval_ts_namespace_chain(&index.target)?; + ts_namespace_member_value((*map).get(name.slice8())?.data) + } + _ => None, + } + } + + /// The member map of the enum or namespace that an unvisited, possibly deep, `a.b.c` names. + fn eval_ts_namespace_chain( + &mut self, + expr: &Expr, + ) -> Option> { + let mut member_names: smallvec::SmallVec<[&[u8]; 4]> = smallvec::SmallVec::new(); + let mut current = *expr; + let root = loop { + match current.data { + js_ast::ExprData::EDot(dot) => { + if dot.optional_chain.is_some() { + return None; + } + member_names.push(dot.name.slice()); + current = dot.target; + } + js_ast::ExprData::EIdentifier(ident) => break ident, + _ => return None, + } + }; + + let name = self.load_name_from_ref(root.ref_); + if name == b"arguments" { + return None; + } + let result = self + .find_symbol_with_record_usage::(current.loc, name) + .ok()?; + if result.is_inside_with_scope || result.r#ref.is_empty() { + return None; + } + let mut data = *self.ref_to_ts_namespace_member.get(&result.r#ref)?; + // The innermost member was pushed last. + for member_name in member_names.iter().rev() { + let js_ast::ts::Data::Namespace(map) = data else { + return None; + }; + data = (*map).get(member_name)?.data; + } + match data { + js_ast::ts::Data::Namespace(map) => Some(map), + _ => None, + } + } + fn s_enum( p: &mut Self, stmts: &mut StmtList<'a>, @@ -2238,6 +2494,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let old_should_fold_typescript_constant_expressions = p.should_fold_typescript_constant_expressions; p.should_fold_typescript_constant_expressions = true; + let old_is_visiting_ts_enum_initializer = p.is_visiting_ts_enum_initializer; + p.is_visiting_ts_enum_initializer = true; // Create an assignment for each enum value for value in values.iter_mut() { @@ -2268,9 +2526,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O next_numeric_value = Some(num.value() + 1.0); } - js_ast::ExprData::EString(str_) => { + js_ast::ExprData::EString(mut str_) => { has_string_value = true; + // Inlined uses share this node's rope and folds append to ropes in place: store it flat. + str_.resolve_rope_if_needed(p.arena); + exported_members.get_ptr_mut(name).unwrap().data = js_ast::ts::Data::EnumString(str_); @@ -2366,6 +2627,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.pop_scope(); p.should_fold_typescript_constant_expressions = old_should_fold_typescript_constant_expressions; + p.is_visiting_ts_enum_initializer = old_is_visiting_ts_enum_initializer; let mut value_stmts: StmtList<'a> = BumpVec::with_capacity_in(value_exprs.len(), p.arena); // Generate statements from expressions diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index a37b8714fbba..5d7b9f60791a 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -57,7 +57,8 @@ bun_core::declare_scope!(cache, visible); /// Version 27: ModuleInfo string table holds Latin-1 / UTF-16 bodies, not WTF-8. /// Version 28: the define table and `--drop` entries participate in the features hash. /// Version 29: `new Array(x, ...spread)` is no longer folded into an array literal. -const EXPECTED_VERSION: u32 = 29; +/// Version 30: TypeScript enum initializers fold references to `const` variables. +const EXPECTED_VERSION: u32 = 30; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/bundler/esbuild/ts.test.ts b/test/bundler/esbuild/ts.test.ts index 4581e85549ec..ad9c6dcefb80 100644 --- a/test/bundler/esbuild/ts.test.ts +++ b/test/bundler/esbuild/ts.test.ts @@ -2356,7 +2356,7 @@ describe("bundler", () => { X25 = (321), // a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation, - /* (we don't implement this one) */ + /* (a bare const identifier folds, see ts/EnumConstVariableReferences below; a namespace member such as ns.x does not) */ // a dotted name that references an enum member with an enum literal type, or X26 = X0, @@ -2514,6 +2514,54 @@ describe("bundler", () => { ]); }, }); + itBundled("ts/EnumConstVariableReferences", { + files: { + "/entry.ts": /* ts */ ` + const base = 100; + const prefix = "app"; + const chained = base + 1, mask = ~chained, route = \`\${prefix}/home\`, joined = prefix + base; + enum Status { Ok = base, Created, Accepted } + enum Tag { Home = prefix, Route = route, Joined = joined, Nested = \`\${route}/x\` } + enum Derived { A = chained, B, C = mask, D = Status.Accepted + 1, E } + const fromEnum = Derived.E * 2; + enum Again { X = fromEnum, Y } + function inner() { + enum Inner { A = declaredAfterInner, B } + return Inner; + } + const declaredAfterInner = 7; + let notConst = 1; + const notConstant = Math.min(4, 2); + enum Computed { A = notConst, B = notConstant } + console.log(JSON.stringify([Status, Tag, Derived, Again, inner(), Computed])); + export { Status, Tag }; + `, + "/other.ts": /* ts */ ` + import { Status, Tag } from "./entry"; + console.log(JSON.stringify([Status.Created, Tag.Route])); + `, + }, + entryPoints: ["/entry.ts", "/other.ts"], + run: [ + { + file: "/out/entry.js", + stdout: + '[{"100":"Ok","101":"Created","102":"Accepted","Ok":100,"Created":101,"Accepted":102},{"Home":"app","Route":"app/home","Joined":"app100","Nested":"app/home/x"},{"101":"A","102":"B","103":"D","104":"E","A":101,"B":102,"C":-102,"-102":"C","D":103,"E":104},{"208":"X","209":"Y","X":208,"Y":209},{"7":"A","8":"B","A":7,"B":8},{"1":"A","2":"B","A":1,"B":2}]', + }, + { + file: "/out/other.js", + stdout: + '[{"100":"Ok","101":"Created","102":"Accepted","Ok":100,"Created":101,"Accepted":102},{"Home":"app","Route":"app/home","Joined":"app100","Nested":"app/home/x"},{"101":"A","102":"B","103":"D","104":"E","A":101,"B":102,"C":-102,"-102":"C","D":103,"E":104},{"208":"X","209":"Y","X":208,"Y":209},{"7":"A","8":"B","A":7,"B":8},{"1":"A","2":"B","A":1,"B":2}]\n[101,"app/home"]', + }, + ], + onAfterBundle(api) { + // The values are known at bundle time, so cross-module uses are inlined too. + const other = api.readFile("/out/other.js"); + expect(other).toContain("101"); + expect(other).toContain('"app/home"'); + expect(other).not.toMatch(/Status\.Created|Tag\.Route/); + }, + }); itBundled("ts/EnumUseBeforeDeclare", { files: { "/entry.ts": /* ts */ ` diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 6ef742958718..dc5a3b1c0855 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -1175,6 +1175,106 @@ function foo() {} `); }); + // Since TypeScript 5.0, an enum member initializer may reference a `const` + // variable whose own initializer is a constant expression. + describe("enum initializers that reference const variables", () => { + const exp = ts.expectPrinted_; + + it("numeric const continues auto-increment", () => { + exp( + "const base = 100;\nenum Status { Ok = base, Created, Accepted }", + 'const base = 100;\nvar Status;\n((Status) => {\n Status[Status["Ok"] = 100] = "Ok";\n Status[Status["Created"] = 101] = "Created";\n Status[Status["Accepted"] = 102] = "Accepted";\n})(Status ||= {})', + ); + }); + + it("string const gets no reverse mapping", () => { + exp( + 'const prefix = "app";\nenum Tag { Home = prefix, About = `${prefix}/about`, Upper = prefix + "!" }', + 'const prefix = "app";\nvar Tag;\n((Tag) => {\n Tag["Home"] = "app";\n Tag["About"] = "app/about";\n Tag["Upper"] = "app!";\n})(Tag ||= {})', + ); + }); + + it("const initializers are evaluated as constant expressions", () => { + exp( + 'enum Base { One = 1 }\nconst a = Base.One + 1, b = -a, c = ~a, d = `${a}${b}`, e = a + "x", f = Base["One"] << 3, inf = -Infinity;\nenum V { A = a, B, C = b, D = c, E = d, F = e, G = f, H = inf }', + 'var Base;\n((Base) => {\n Base[Base["One"] = 1] = "One";\n})(Base ||= {});\nconst a = 1 /* One */ + 1, b = -a, c = ~a, d = `${a}${b}`, e = a + "x", f = 1 /* One */ << 3, inf = -1 / 0;\nvar V;\n((V) => {\n V[V["A"] = 2] = "A";\n V[V["B"] = 3] = "B";\n V[V["C"] = -2] = "C";\n V[V["D"] = -3] = "D";\n V["E"] = "2-2";\n V["F"] = "2x";\n V[V["G"] = 8] = "G";\n V[V["H"] = -1 / 0] = "H";\n})(V ||= {})', + ); + }); + + it("leaves alone what TypeScript does not fold", () => { + // declared after the enum, not const, not a constant expression, or declared with a type annotation + exp( + "let x = 1;\nconst y = Math.PI;\nconst z: number = 3;\nenum E { A = later, B = x, C = y, D = z, F }\nconst later = 5;", + 'let x = 1;\nconst y = Math.PI;\nconst z = 3;\nvar E;\n((E) => {\n E[E["A"] = later] = "A";\n E[E["B"] = x] = "B";\n E[E["C"] = y] = "C";\n E[E["D"] = z] = "D";\n E[E["F"] = undefined] = "F";\n})(E ||= {});\nconst later = 5', + ); + // shadowed by a hoisted variable in the enum's scope + exp( + "const v = 1;\nfunction g() {\n enum E { A = v }\n var v = 2;\n return E;\n}", + 'const v = 1;\nfunction g() {\n let E;\n ((E) => {\n E[E["A"] = v] = "A";\n })(E ||= {});\n var v = 2;\n return E;\n}', + ); + // the const is only substituted inside enum initializers + exp( + "const n = 5;\nenum E { A = n }\nconsole.log(n, E.A);", + 'const n = 5;\nvar E;\n((E) => {\n E[E["A"] = 5] = "A";\n})(E ||= {});\nconsole.log(n, 5 /* A */)', + ); + }); + + it("a function body sees an outer const declared after it", () => { + exp( + "export function f() {\n enum E { A = outer, B }\n return E;\n}\nconst outer = 7;", + 'export function f() {\n let E;\n ((E) => {\n E[E["A"] = 7] = "A";\n E[E["B"] = 8] = "B";\n })(E ||= {});\n return E;\n}\nconst outer = 7', + ); + }); + + it("exported const inside a namespace", () => { + exp( + "namespace N {\n export const a = 1;\n export enum E { X = a, Y }\n}", + 'var N;\n((N) => {\n N.a = 1;\n let E;\n ((E) => {\n E[E["X"] = 1] = "X";\n E[E["Y"] = 2] = "Y";\n })(E = N.E ||= {});\n})(N ||= {})', + ); + }); + + it("enum nested in a namespace, read through a dotted name", () => { + // The namespace body is visited before the function body, so the member value is known there. + exp( + "namespace N { export enum E { A = 4 } }\nexport function f() {\n const c = N.E.A + 1;\n enum X { Q = c, R }\n return X;\n}", + 'var N;\n((N) => {\n let E;\n ((E) => {\n E[E["A"] = 4] = "A";\n })(E = N.E ||= {});\n})(N ||= {});\nexport function f() {\n const c = 4 /* A */ + 1;\n let X;\n ((X) => {\n X[X["Q"] = 5] = "Q";\n X[X["R"] = 6] = "R";\n })(X ||= {});\n return X;\n}', + ); + }); + + it("a long dotted name in a const initializer is walked without recursion", () => { + // The visitor still rejects the nesting depth; the const pre-pass must not overflow the stack first. + const code = + "declare const a: any;\nconst x = a" + Buffer.alloc(100_000, ".b").toString() + ";\nenum E { A = 1 }"; + expect(() => ts.parsed(code, false, false)).toThrow("Maximum call stack size exceeded"); + }); + + // tsc folds a dotted name to a namespace's exported const too. Not implemented: + // the namespace member map does not carry the value. + it.todo("const reached through a namespace", () => { + exp( + "namespace N { export const a = 1; }\nenum E { X = N.a, Y }", + 'var N;\n((N) => {\n N.a = 1;\n})(N ||= {});\nvar E;\n((E) => {\n E[E["X"] = 1] = "X";\n E[E["Y"] = 2] = "Y";\n})(E ||= {})', + ); + }); + + it("folds through to uses of the member", () => { + // https://github.com/oven-sh/bun/issues/19581 + const out = ts.parsedMin( + "const enum First { A = 1, B = 2, C = 3 }\nconst multiplier = 5;\nconst enum Second { D = First.A * multiplier, E = First.B * multiplier, F = First.C * multiplier }\nconsole.log(Second.E + Second.F);", + ); + expect(out.split("\n").at(-1)).toBe("console.log(25)"); + }); + + it("a folded string member can be read by more than one template literal", () => { + // The member is stored flat. A rope would be shared by every inlined copy, + // and folding the first template literal would append to it. + ts.expectPrintedMin_( + 'const prefix = "app";\nenum Tag { Upper = prefix + "!" }\nconsole.log(`${Tag.Upper}x`, `${Tag.Upper}y`);', + 'const prefix = "app";\nvar Tag;\n((Tag) => Tag.Upper = "app!")(Tag ||= {});\nconsole.log("app!x", "app!y")', + ); + }); + }); + // TODO: fix all the cases that report generic "Parse error" it("types", () => { const exp = ts.expectPrinted_;