diff --git a/Cargo.lock b/Cargo.lock index 6153b50b3f9b..53eb483735f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10398,6 +10398,7 @@ dependencies = [ "strsim 0.11.1", "swc_core", "swc_sourcemap", + "thread_local", "tokio", "tracing", "turbo-bincode", diff --git a/crates/next-core/src/segment_config.rs b/crates/next-core/src/segment_config.rs index 08850253ae3d..06ff03170bc2 100644 --- a/crates/next-core/src/segment_config.rs +++ b/crates/next-core/src/segment_config.rs @@ -30,7 +30,9 @@ use turbopack_core::{ }; use turbopack_ecmascript::{ EcmascriptInputTransforms, EcmascriptModuleAssetType, - analyzer::{ConstantNumber, ConstantValue, JsValue, ObjectPart, graph::EvalContext}, + analyzer::{ + Bump, ConstantNumber, ConstantValue, JsValue, ObjectPart, ThreadLocal, graph::EvalContext, + }, parse::{ParseResult, parse}, }; @@ -414,12 +416,25 @@ pub async fn parse_segment_config_from_source( return Ok(Default::default()); }; + // Arena for the `JsValue`s produced while evaluating config expressions; + // freed when this function returns. + let arena = ThreadLocal::new(); let config = WrapFuture::new( async { let mut config = NextSegmentConfig::default(); let mut parse = async |ident, init, span| { - parse_config_value(source, mode, &mut config, eval_context, ident, init, span).await + parse_config_value( + source, + mode, + &mut config, + eval_context, + &arena, + ident, + init, + span, + ) + .await }; for item in &module_ast.body { @@ -576,7 +591,7 @@ async fn invalid_config( key: &str, span: Span, error: RcStr, - value: Option<&JsValue>, + value: Option<&JsValue<'_>>, severity: IssueSeverity, ) -> Result<()> { let detail = if let Some(value) = value { @@ -605,6 +620,7 @@ async fn parse_config_value( mode: ParseSegmentMode, config: &mut NextSegmentConfig, eval_context: &EvalContext, + arena: &ThreadLocal, key: Cow<'_, str>, init: Option>, span: Span, @@ -620,17 +636,18 @@ async fn parse_config_value( | Some(Expr::TsSatisfies(TsSatisfiesExpr { expr, .. })) => Some(&**expr), _ => init, }; - init.map(|init| eval_context.eval(init)).map(|v| { - // Special case, as we don't call `link` here: assume that `undefined` is a free - // variable. - if let JsValue::FreeVar(name) = &v - && name == "undefined" - { - JsValue::Constant(ConstantValue::Undefined) - } else { - v - } - }) + init.map(|init| eval_context.eval(arena.get_or_default(), init)) + .map(|v| { + // Special case, as we don't call `link` here: assume that `undefined` is a free + // variable. + if let JsValue::FreeVar(name) = &v + && name == "undefined" + { + JsValue::Constant(ConstantValue::Undefined) + } else { + v + } + }) }; match &*key { @@ -1002,7 +1019,7 @@ async fn parse_static_string_or_array_from_js_value( span: Span, key: &str, sub_key: &str, - value: &JsValue, + value: &JsValue<'_>, ) -> Result>> { Ok(match value { // Single value is turned into a single-element Vec. @@ -1055,129 +1072,130 @@ async fn parse_static_string_or_array_from_js_value( async fn parse_route_matcher_from_js_value( source: ResolvedVc>, span: Span, - value: &JsValue, + value: &JsValue<'_>, ) -> Result>> { - let parse_matcher_kind_matcher = async |value: &JsValue, sub_key: &str, matcher_idx: usize| { - let mut route_has = vec![]; - if let JsValue::Array { items, .. } = value { - for (i, item) in items.iter().enumerate() { - if let JsValue::Object { parts, .. } = item { - let mut route_type = None; - let mut route_key = None; - let mut route_value = None; - - for matcher_part in parts { - if let ObjectPart::KeyValue(part_key, part_value) = matcher_part { - match part_key.as_str() { - Some("type") => { - if let Some(part_value) = part_value.as_str().filter(|v| { - *v == "header" - || *v == "cookie" - || *v == "query" - || *v == "host" - }) { - route_type = Some(part_value); - } else { - invalid_config( - source, - "config", - span, - format!( - "`matcher[{matcher_idx}].{sub_key}[{i}].type` \ - must be one of the strings: 'header', 'cookie', \ - 'query', 'host'" + let parse_matcher_kind_matcher = + async |value: &JsValue<'_>, sub_key: &str, matcher_idx: usize| { + let mut route_has = vec![]; + if let JsValue::Array { items, .. } = value { + for (i, item) in items.iter().enumerate() { + if let JsValue::Object { parts, .. } = item { + let mut route_type = None; + let mut route_key = None; + let mut route_value = None; + + for matcher_part in parts { + if let ObjectPart::KeyValue(part_key, part_value) = matcher_part { + match part_key.as_str() { + Some("type") => { + if let Some(part_value) = part_value.as_str().filter(|v| { + *v == "header" + || *v == "cookie" + || *v == "query" + || *v == "host" + }) { + route_type = Some(part_value); + } else { + invalid_config( + source, + "config", + span, + format!( + "`matcher[{matcher_idx}].{sub_key}[{i}].type` \ + must be one of the strings: 'header', \ + 'cookie', 'query', 'host'" + ) + .into(), + Some(part_value), + IssueSeverity::Error, ) - .into(), - Some(part_value), - IssueSeverity::Error, - ) - .await?; + .await?; + } } - } - Some("key") => { - if let Some(part_value) = part_value.as_str() { - route_key = Some(part_value); - } else { - invalid_config( - source, - "config", - span, - format!( - "`matcher[{matcher_idx}].{sub_key}[{i}].key` must \ - be a string" + Some("key") => { + if let Some(part_value) = part_value.as_str() { + route_key = Some(part_value); + } else { + invalid_config( + source, + "config", + span, + format!( + "`matcher[{matcher_idx}].{sub_key}[{i}].key` \ + must be a string" + ) + .into(), + Some(part_value), + IssueSeverity::Error, ) - .into(), - Some(part_value), - IssueSeverity::Error, - ) - .await?; + .await?; + } } - } - Some("value") => { - if let Some(part_value) = part_value.as_str() { - route_value = Some(part_value); - } else { + Some("value") => { + if let Some(part_value) = part_value.as_str() { + route_value = Some(part_value); + } else { + invalid_config( + source, + "config", + span, + format!( + "`matcher[{matcher_idx}].{sub_key}[{i}].\ + value` must be a string" + ) + .into(), + Some(part_value), + IssueSeverity::Error, + ) + .await?; + } + } + _ => { invalid_config( source, "config", span, format!( - "`matcher[{matcher_idx}].{sub_key}[{i}].value` \ - must be a string" + "Unexpected property in \ + `matcher[{matcher_idx}].{sub_key}[{i}]` object" ) .into(), - Some(part_value), + Some(part_key), IssueSeverity::Error, ) .await?; } } - _ => { - invalid_config( - source, - "config", - span, - format!( - "Unexpected property in \ - `matcher[{matcher_idx}].{sub_key}[{i}]` object" - ) - .into(), - Some(part_key), - IssueSeverity::Error, - ) - .await?; - } } } - } - let r = match route_type { - Some("header") => route_key.map(|route_key| RouteHas::Header { - key: route_key.into(), - value: route_value.map(From::from), - }), - Some("cookie") => route_key.map(|route_key| RouteHas::Cookie { - key: route_key.into(), - value: route_value.map(From::from), - }), - Some("query") => route_key.map(|route_key| RouteHas::Query { - key: route_key.into(), - value: route_value.map(From::from), - }), - Some("host") => route_value.map(|route_value| RouteHas::Host { - value: route_value.into(), - }), - _ => None, - }; - - if let Some(r) = r { - route_has.push(r); + let r = match route_type { + Some("header") => route_key.map(|route_key| RouteHas::Header { + key: route_key.into(), + value: route_value.map(From::from), + }), + Some("cookie") => route_key.map(|route_key| RouteHas::Cookie { + key: route_key.into(), + value: route_value.map(From::from), + }), + Some("query") => route_key.map(|route_key| RouteHas::Query { + key: route_key.into(), + value: route_value.map(From::from), + }), + Some("host") => route_value.map(|route_value| RouteHas::Host { + value: route_value.into(), + }), + _ => None, + }; + + if let Some(r) = r { + route_has.push(r); + } } } } - } - anyhow::Ok(route_has) - }; + anyhow::Ok(route_has) + }; let mut matchers = vec![]; diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index 6182c6589bf1..13f1e427c6bd 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -46,6 +46,7 @@ serde_json = { workspace = true, features = ["raw_value"] } swc_sourcemap = { workspace = true } smallvec = { workspace = true } strsim = { workspace = true } +thread_local = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } turbo-bincode = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript/benches/analyzer.rs b/turbopack/crates/turbopack-ecmascript/benches/analyzer.rs index 4fbbae2027b2..4d0b8b1d82bf 100644 --- a/turbopack/crates/turbopack-ecmascript/benches/analyzer.rs +++ b/turbopack/crates/turbopack-ecmascript/benches/analyzer.rs @@ -25,6 +25,7 @@ use turbopack_core::{ use turbopack_ecmascript::{ AnalyzeMode, analyzer::{ + Bump, ThreadLocal, graph::{EvalContext, VarGraph, create_graph}, imports::ImportAttributes, linker::link, @@ -72,7 +73,11 @@ pub fn benchmark(c: &mut Criterion) { Default::default(), None, ); + // Leak a per-benchmark arena so the stored `VarGraph` can be `'static` (benches are + // short-lived processes, so the leak is inconsequential). + let arena: &'static ThreadLocal = Box::leak(Box::new(ThreadLocal::new())); let var_graph = Arc::new(create_graph( + arena.get_or_default(), &program, &eval_context, AnalyzeMode::CodeGenerationAndTracing, @@ -83,6 +88,7 @@ pub fn benchmark(c: &mut Criterion) { program, eval_context, var_graph, + arena, }; group.bench_with_input( @@ -99,17 +105,20 @@ pub fn benchmark(c: &mut Criterion) { struct BenchInput { program: Program, eval_context: EvalContext, - var_graph: Arc, + var_graph: Arc>, + arena: &'static ThreadLocal, } fn bench_create_graph(b: &mut Bencher, input: &BenchInput) { b.iter(|| { - create_graph( + let arena = ThreadLocal::new(); + criterion::black_box(create_graph( + arena.get_or_default(), &input.program, &input.eval_context, AnalyzeMode::CodeGenerationAndTracing, true, - ) + )); }); } @@ -117,6 +126,8 @@ fn bench_link(b: &mut Bencher, input: &BenchInput) { let rt = tokio::runtime::Builder::new_current_thread() .build() .unwrap(); + + let arena = input.arena; let var_graph = input.var_graph.clone(); b.to_async(rt).iter_custom(move |iters| { @@ -150,10 +161,18 @@ fn bench_link(b: &mut Bencher, input: &BenchInput) { let var_cache = Default::default(); for value in var_graph.values.values() { link( + arena, &var_graph, - value.clone(), - &early_visitor, - &(|val| visitor(val, compile_time_info, ImportAttributes::empty_ref())), + value.clone_in(arena.get_or_default()), + &(|val| early_visitor(arena, val)), + &(|val| { + visitor( + arena, + val, + compile_time_info, + ImportAttributes::empty_ref(), + ) + }), &Default::default(), &var_cache, ) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/builtin.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/builtin.rs index f6c45aee7215..b24417ef220f 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/builtin.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/builtin.rs @@ -1,14 +1,15 @@ use std::mem::take; +use smallvec::SmallVec; use turbo_rcstr::rcstr; use super::{ConstantNumber, ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart}; -use crate::analyzer::JsValueUrlKind; +use crate::analyzer::{Bump, BumpVec, JsValueUrlKind}; /// Replaces some builtin values with their resulting values. Called early /// without lazy nested values. This allows to skip a lot of work to process the /// arguments. -pub fn early_replace_builtin(value: &mut JsValue) -> bool { +pub fn early_replace_builtin(value: &mut JsValue<'_>) -> bool { match value { // matching calls like `callee(arg1, arg2, ...)` JsValue::Call(_, call) => { @@ -77,18 +78,17 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool { } // matching property access like `obj.prop` when we don't know what the obj is. // We can early return here - &mut JsValue::Member( - _, - box JsValue::Unknown { - original_value: _, - reason: _, - has_side_effects, - }, - box ref mut prop, - ) => { - let side_effects = has_side_effects || prop.has_side_effects(); - value.make_unknown(side_effects, rcstr!("unknown object")); - true + JsValue::Member(_, obj, prop) => { + if let JsValue::Unknown { + has_side_effects, .. + } = &**obj + { + let side_effects = *has_side_effects || prop.has_side_effects(); + value.make_unknown(side_effects, rcstr!("unknown object")); + true + } else { + false + } } _ => false, } @@ -97,7 +97,7 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool { /// Replaces some builtin functions and values with their resulting values. In /// contrast to early_replace_builtin this has all inner values already /// processed. -pub fn replace_builtin(value: &mut JsValue) -> bool { +pub fn replace_builtin<'a>(arena: &'a Bump, value: &mut JsValue<'a>) -> bool { match value { JsValue::Add(_, list) => { // numeric addition @@ -114,7 +114,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { // matching property access like `obj.prop` // Accessing a property on something can be handled in some cases - JsValue::Member(_, box obj, prop) => match obj { + JsValue::Member(_, obj, prop) => match &mut **obj { // matching property access when obj is a bunch of alternatives // like `(obj1 | obj2 | obj3).prop` // We expand these to `obj1.prop | obj2.prop | obj3.prop` @@ -123,12 +123,12 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { values, logical_property: _, } => { - *value = JsValue::alternatives( + *value = JsValue::alternatives(BumpVec::from_iter_in( + arena, take(values) .into_iter() - .map(|alt| JsValue::member(Box::new(alt), prop.clone())) - .collect(), - ); + .map(|alt| JsValue::member(arena, alt, prop.clone_in(arena))), + )); true } // matching property access on an array like `[1,2,3].prop` or `[1,2,3][1]` @@ -137,9 +137,13 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { mutable, .. } => { - fn items_to_alternatives(items: &mut Vec, prop: &mut JsValue) -> JsValue { - items.push(JsValue::unknown( - JsValue::member(Box::new(JsValue::array(Vec::new())), Box::new(take(prop))), + fn items_to_alternatives<'a>( + arena: &'a Bump, + items: &mut BumpVec<'a, JsValue<'a>>, + prop: &mut JsValue<'a>, + ) -> JsValue<'a> { + items.push(arena, JsValue::unknown( + JsValue::member(arena, JsValue::array(BumpVec::new()), take(prop)), false, rcstr!("unknown array prototype methods or values"), )); @@ -153,12 +157,12 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { if index < items.len() { *value = items.swap_remove(index); if mutable { - value.add_unknown_mutations(true); + value.add_unknown_mutations(arena, true); } true } else { *value = JsValue::unknown( - JsValue::member(Box::new(take(obj)), Box::new(take(prop))), + JsValue::member(arena, take(&mut **obj), take(&mut **prop)), false, rcstr!("invalid index"), ); @@ -182,18 +186,18 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { values, logical_property: _, } => { - *value = JsValue::alternatives( + *value = JsValue::alternatives(BumpVec::from_iter_in( + arena, take(values) .into_iter() - .map(|alt| JsValue::member(Box::new(obj.clone()), Box::new(alt))) - .collect(), - ); + .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)), + )); true } // otherwise we can say that this might gives an item of the array // but we also add an unknown value to the alternatives for other properties _ => { - *value = items_to_alternatives(items, prop); + *value = items_to_alternatives(arena, items, prop); true } } @@ -204,22 +208,28 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { mutable, .. } => { - fn parts_to_alternatives( - parts: &mut Vec, - prop: &mut Box, + fn parts_to_alternatives<'a>( + arena: &'a Bump, + parts: impl IntoIterator>, + prop: &mut JsValue<'a>, include_unknown: bool, - ) -> JsValue { - let mut values = Vec::new(); + ) -> JsValue<'a> { + let parts = parts.into_iter(); + let (lower, upper) = parts.size_hint(); + let mut values = BumpVec::with_capacity_in( + arena, upper.unwrap_or(lower) + if include_unknown { 1 } else { 0 } + ); for part in parts { match part { ObjectPart::KeyValue(_, value) => { - values.push(take(value)); + values.push(arena, value); } ObjectPart::Spread(_) => { - values.push(JsValue::unknown( + values.push(arena, JsValue::unknown( JsValue::member( - Box::new(JsValue::object(vec![take(part)])), - prop.clone(), + arena, + JsValue::object(BumpVec::from_iter_in(arena, [part])), + prop.clone_in(arena), ), true, rcstr!("spread object"), @@ -228,10 +238,11 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { } } if include_unknown { - values.push(JsValue::unknown( + values.push(arena, JsValue::unknown( JsValue::member( - Box::new(JsValue::object(Vec::new())), - Box::new(take(prop)), + arena, + JsValue::object(BumpVec::new()), + take(prop), ), true, rcstr!("unknown object prototype methods or values"), @@ -244,12 +255,13 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { /// JsValue::Alternatives Optionally add a /// unknown value to the alternatives for object prototype /// methods - fn potential_values_to_alternatives( - mut potential_values: Vec, - parts: &mut Vec, - prop: &mut Box, + fn potential_values_to_alternatives<'a>( + arena: &'a Bump, + mut potential_values: SmallVec<[usize; 8]>, + parts: &mut BumpVec<'a, ObjectPart<'a>>, + prop: &mut JsValue<'a>, include_unknown: bool, - ) -> JsValue { + ) -> JsValue<'a> { // Note: potential_values are already in reverse order let mut potential_values = take(parts) .into_iter() @@ -262,9 +274,8 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { false } }) - .map(|(_, part)| part) - .collect(); - parts_to_alternatives(&mut potential_values, prop, include_unknown) + .map(|(_, part)| part); + parts_to_alternatives(arena, &mut potential_values, prop, include_unknown) } match &mut **prop { @@ -272,7 +283,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { // 2}["a"]` JsValue::Constant(ConstantValue::Str(_)) => { let prop_str = prop.as_str().unwrap(); - let mut potential_values = Vec::new(); + let mut potential_values: SmallVec<[usize; 8]> = SmallVec::new(); for (i, part) in parts.iter_mut().enumerate().rev() { match part { ObjectPart::KeyValue(key, val) => { @@ -283,6 +294,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { } else { potential_values.push(i); *value = potential_values_to_alternatives( + arena, potential_values, parts, prop, @@ -290,7 +302,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { ); } if mutable { - value.add_unknown_mutations(true); + value.add_unknown_mutations(arena, true); } return true; } @@ -308,6 +320,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { *value = JsValue::Constant(ConstantValue::Undefined); } else { *value = potential_values_to_alternatives( + arena, potential_values, parts, prop, @@ -315,7 +328,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { ); } if mutable { - value.add_unknown_mutations(true); + value.add_unknown_mutations(arena, true); } true } @@ -326,16 +339,16 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { values, logical_property: _, } => { - *value = JsValue::alternatives( + *value = JsValue::alternatives(BumpVec::from_iter_in( + arena, take(values) .into_iter() - .map(|alt| JsValue::member(Box::new(obj.clone()), Box::new(alt))) - .collect(), - ); + .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)), + )); true } _ => { - *value = parts_to_alternatives(parts, prop, true); + *value = parts_to_alternatives(arena, take(parts), prop, true); true } } @@ -343,10 +356,14 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { _ => false, }, - JsValue::MemberCall(_, call) => { + JsValue::MemberCall(_, _) => { // `into_parts` pops obj + prop off the tail of the underlying `Vec`, and the - // remaining `Vec` (owned, not reallocated) becomes `args`. - let (mut obj, prop, args) = take(call).into_parts(); + // remaining `Vec` (owned, not reallocated) becomes `args`. We take the whole + // `value` because `MemberCallList` has no `Default` to move it out directly. + let JsValue::MemberCall(_, call) = take(value) else { + unreachable!() + }; + let (mut obj, prop, args) = call.into_parts(); match &mut obj { // matching calls on an array like `[1,2,3].concat([4,5,6])` JsValue::Array { items, mutable, .. } => { @@ -375,7 +392,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { mutable: inner_mutable, .. } => { - items.extend(inner); + items.extend(arena, inner); *mutable |= inner_mutable; } other @ (JsValue::Constant(_) @@ -385,7 +402,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { | JsValue::WellKnownObject(_) | JsValue::WellKnownFunction(_) | JsValue::Function(..)) => { - items.push(other); + items.push(arena, other); } _ => { unreachable!(); @@ -399,23 +416,21 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { // The Array.prototype.map method "map" => { if let Some(func) = args.first() { - *value = JsValue::array( - take(items) - .into_iter() - .enumerate() - .map(|(i, item)| { - JsValue::call_from_iter( - func.clone(), - [ - item, - JsValue::Constant(ConstantValue::Num( - (i as f64).into(), - )), - ], - ) - }) - .collect(), - ); + *value = JsValue::array(BumpVec::from_iter_in( + arena, + take(items).into_iter().enumerate().map(|(i, item)| { + JsValue::call_from_iter( + arena, + func.clone_in(arena), + [ + item, + JsValue::Constant(ConstantValue::Num( + (i as f64).into(), + )), + ], + ) + }), + )); return true; } } @@ -430,18 +445,17 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { values, logical_property: _, } => { - *value = JsValue::alternatives( - take(values) - .into_iter() - .map(|alt| { - JsValue::member_call_from_iter( - alt, - prop.clone(), - args.iter().cloned(), - ) - }) - .collect(), - ); + *value = JsValue::alternatives(BumpVec::from_iter_in( + arena, + take(values).into_iter().map(|alt| { + JsValue::member_call_from_iter( + arena, + alt, + prop.clone_in(arena), + args.iter().map(|a| a.clone_in(arena)), + ) + }, + ))); return true; } _ => {} @@ -453,8 +467,9 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { { // The String.prototype.concat method if str == "concat" { - let mut values = vec![obj]; - values.extend(args); + let mut values = BumpVec::with_capacity_in(arena, 1 + args.len()); + values.push(arena, obj); + values.extend(arena, args); *value = JsValue::concat(values); return true; @@ -469,10 +484,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { // into a `JsValue::Call` only needs `+1` slot, which fits in the existing slack — // no realloc. This is the original motivation for the `[args..., prop, obj]` // tail layout. - *value = JsValue::call_from_parts( - JsValue::member(Box::new(obj), Box::new(prop)), - args, - ); + *value = JsValue::call_from_parts(arena, JsValue::member(arena, obj, prop), args); true } // match calls when the callee are multiple alternative functions like `(func1 | @@ -480,17 +492,20 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { JsValue::Call(_, call) if matches!(call.callee(), JsValue::Alternatives { .. }) => { - // Take ownership so we can move the alternatives `values` out of the callee. - let (callee, args) = take(call).into_parts(); + // Take the whole `value` (not `call`) because `CallList` has no `Default`, then + // move the alternatives `values` out of the callee. + let JsValue::Call(_, call) = take(value) else { + unreachable!() + }; + let (callee, args) = call.into_parts(); let JsValue::Alternatives { values, .. } = callee else { unreachable!() }; - *value = JsValue::alternatives( + *value = JsValue::alternatives(BumpVec::from_iter_in(arena, values .into_iter() - .map(|alt| JsValue::call_from_iter(alt, args.iter().cloned())) - .collect(), - ); + .map(|alt| JsValue::call_from_iter(arena, alt, args.iter().map(|a| a.clone_in(arena)))), + )); true } // match object literals @@ -508,10 +523,10 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { .. }) = part { - parts.extend(inner_parts); + parts.extend(arena, inner_parts); *mutable |= inner_mutable; } else { - parts.push(part); + parts.push(arena, part); } } value.update_total_nodes(); @@ -519,17 +534,19 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { } // match logical expressions like `a && b` or `a || b || c` or `a ?? b` // Reduce logical expressions to their final value(s) - JsValue::Logical(_, op, parts) => { - let len = parts.len(); - let input_parts: Vec = take(parts); - *parts = Vec::with_capacity(len); + JsValue::Logical(..) => { + let JsValue::Logical(_, op, input_parts) = take(value) else { + unreachable!() + }; + let len = input_parts.len(); + let mut parts = BumpVec::>::with_capacity_in(arena, len); let mut part_properties = Vec::with_capacity(len); for (i, part) in input_parts.into_iter().enumerate() { // The last part is never skipped. if i == len - 1 { // We intentionally omit the part_properties for the last part. // This isn't always needed so we only compute it when actually needed. - parts.push(part); + parts.push(arena, part); break; } let property = match op { @@ -546,13 +563,13 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { Some(false) => { // We known this part is the final value, so we can remove the rest. part_properties.push(property); - parts.push(part); + parts.push(arena, part); break; } None => { // We don't know if this part is skipped or the final value, so we keep it. part_properties.push(property); - parts.push(part); + parts.push(arena, part); continue; } } @@ -608,20 +625,20 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { } }; if let Some(property) = property { - *value = JsValue::alternatives_with_additional_property(take(parts), property); + *value = JsValue::alternatives_with_additional_property(parts, property); true } else { - *value = JsValue::alternatives(take(parts)); + *value = JsValue::alternatives(parts); true } } } JsValue::Tenary(_, test, cons, alt) => { if test.is_truthy() == Some(true) { - *value = take(cons); + *value = take(&mut **cons); true } else if test.is_falsy() == Some(true) { - *value = take(alt); + *value = take(&mut **alt); true } else { false @@ -659,7 +676,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { if let JsValue::Array { items, mutable, .. } = &mut **iterable { let mut new_value = JsValue::alternatives(take(items)); if *mutable { - new_value.add_unknown_mutations(true); + new_value.add_unknown_mutations(arena, true); } *value = new_value; true @@ -670,10 +687,10 @@ pub fn replace_builtin(value: &mut JsValue) -> bool { JsValue::Awaited(_, operand) => { if let JsValue::Promise(_, inner) = &mut **operand { - *value = take(inner); + *value = take(&mut **inner); true } else { - *value = take(operand); + *value = take(&mut **operand); true } } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/bump_vec.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/bump_vec.rs index 9550ba849170..53ceb36d9ee2 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/bump_vec.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/bump_vec.rs @@ -168,6 +168,19 @@ impl<'a, T> BumpVec<'a, T> { Some(unsafe { self.ptr.as_ptr().add(self.len).read() }) } + /// Remove the element at `index` and return it, moving the last element into its slot. This + /// does not preserve element order but is O(1). Panics if `index` is out of bounds. + pub fn swap_remove(&mut self, index: usize) -> T { + assert!(index < self.len, "swap_remove index out of bounds"); + // Move the last item into the slot and return the displaced one. + let last = self.pop().unwrap(); + if index < self.len { + std::mem::replace(&mut self[index], last) + } else { + last + } + } + /// Split the vec in two at `at`: `self` retains the prefix `[0, at)` and the returned vec owns /// the suffix `[at, len)`, moved into a fresh arena allocation. pub fn split_off(&mut self, bump: &'a Bump, at: usize) -> Self { @@ -280,8 +293,15 @@ impl Iterator for IntoIter<'_, T> { self.idx += 1; Some(value) } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.idx; + (remaining, Some(remaining)) + } } +impl ExactSizeIterator for IntoIter<'_, T> {} + impl Drop for IntoIter<'_, T> { fn drop(&mut self) { // SAFETY: indices `[idx, len)` are still initialized; drop them in place exactly once. @@ -405,6 +425,45 @@ mod tests { assert!(v.is_empty()); } + #[test] + fn swap_remove() { + let bump = Bump::new(); + let mut v = BumpVec::from_iter_in(&bump, [1, 2, 3, 4, 5]); + // Removing a middle element moves the last element into its slot. + assert_eq!(v.swap_remove(1), 2); + assert_eq!(&*v, &[1, 5, 3, 4][..]); + // Removing the last element is just a pop. + assert_eq!(v.swap_remove(3), 4); + assert_eq!(&*v, &[1, 5, 3][..]); + // Removing the first element. + assert_eq!(v.swap_remove(0), 1); + assert_eq!(&*v, &[3, 5][..]); + } + + #[test] + #[should_panic(expected = "swap_remove index out of bounds")] + fn swap_remove_out_of_bounds() { + let bump = Bump::new(); + let mut v = BumpVec::from_iter_in(&bump, [1, 2, 3]); + v.swap_remove(3); + } + + #[test] + fn swap_remove_does_not_double_free() { + let bump = Bump::new(); + let counter = Rc::new(Cell::new(0)); + let mut v = BumpVec::new(); + for _ in 0..3 { + v.push(&bump, DropCounter(counter.clone())); + } + let removed = v.swap_remove(0); + drop(removed); + assert_eq!(counter.get(), 1); + // The two remaining drop exactly once; the removed slot must not be dropped again. + drop(v); + assert_eq!(counter.get(), 3); + } + #[test] fn split_off_prefix_and_suffix() { let bump = Bump::new(); diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/effects.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/effects.rs index 1a327db2f07a..6f5bb8a7cb88 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/effects.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/effects.rs @@ -1,56 +1,60 @@ +use bumpalo::boxed::Box as BumpBox; use swc_core::{atoms::Atom, common::Span, ecma::visit::fields::*}; use turbo_rcstr::RcStr; use turbopack_core::resolve::ExportUsage; -use crate::{analyzer::JsValue, utils::AstPathRange}; +use crate::{ + analyzer::{Bump, JsValue}, + utils::AstPathRange, +}; #[derive(Debug)] -pub struct EffectsBlock { - pub effects: Vec, +pub struct EffectsBlock<'a> { + pub effects: Vec>, pub range: AstPathRange, } -impl EffectsBlock { +impl EffectsBlock<'_> { pub fn is_empty(&self) -> bool { self.effects.is_empty() } } #[derive(Debug)] -pub enum ConditionalKind { +pub enum ConditionalKind<'a> { /// The blocks of an `if` statement without an `else` block. - If { then: Box }, + If { then: Box> }, /// The blocks of an `if ... else` or `if { ... return ... } ...` statement. IfElse { - then: Box, - r#else: Box, + then: Box>, + r#else: Box>, }, /// The blocks of an `if ... else` statement. - Else { r#else: Box }, + Else { r#else: Box> }, /// The blocks of an `if { ... return ... } else { ... } ...` or `if { ... } /// else { ... return ... } ...` statement. IfElseMultiple { - then: Vec>, - r#else: Vec>, + then: Vec>>, + r#else: Vec>>, }, /// The expressions on the right side of the `?:` operator. Ternary { - then: Box, - r#else: Box, + then: Box>, + r#else: Box>, }, /// The expression on the right side of the `&&` operator. - And { expr: Box }, + And { expr: Box> }, /// The expression on the right side of the `||` operator. - Or { expr: Box }, + Or { expr: Box> }, /// The expression on the right side of the `??` operator. - NullishCoalescing { expr: Box }, + NullishCoalescing { expr: Box> }, /// The expression on the right side of a labeled statement. - Labeled { body: Box }, + Labeled { body: Box> }, } -impl ConditionalKind { +impl<'a> ConditionalKind<'a> { /// Normalizes all contained values. - pub fn normalize(&mut self) { + pub fn normalize(&mut self, arena: &'a Bump) { match self { ConditionalKind::If { then: block } | ConditionalKind::Else { r#else: block } @@ -58,28 +62,28 @@ impl ConditionalKind { | ConditionalKind::Or { expr: block, .. } | ConditionalKind::NullishCoalescing { expr: block, .. } => { for effect in &mut block.effects { - effect.normalize(); + effect.normalize(arena); } } ConditionalKind::IfElse { then, r#else, .. } | ConditionalKind::Ternary { then, r#else, .. } => { for effect in &mut then.effects { - effect.normalize(); + effect.normalize(arena); } for effect in &mut r#else.effects { - effect.normalize(); + effect.normalize(arena); } } ConditionalKind::IfElseMultiple { then, r#else, .. } => { for block in then.iter_mut().chain(r#else.iter_mut()) { for effect in &mut block.effects { - effect.normalize(); + effect.normalize(arena); } } } ConditionalKind::Labeled { body } => { for effect in &mut body.effects { - effect.normalize(); + effect.normalize(arena); } } } @@ -87,21 +91,21 @@ impl ConditionalKind { } #[derive(Debug)] -pub enum EffectArg { - Value(JsValue), - Closure(JsValue, Box), +pub enum EffectArg<'a> { + Value(JsValue<'a>), + Closure(JsValue<'a>, Box>), Spread, } -impl EffectArg { +impl<'a> EffectArg<'a> { /// Normalizes all contained values. - pub fn normalize(&mut self) { + pub fn normalize(&mut self, arena: &'a Bump) { match self { - EffectArg::Value(value) => value.normalize(), + EffectArg::Value(value) => value.normalize(arena), EffectArg::Closure(value, effects) => { - value.normalize(); + value.normalize(arena); for effect in &mut effects.effects { - effect.normalize(); + effect.normalize(arena); } } EffectArg::Spread => {} @@ -110,21 +114,21 @@ impl EffectArg { } #[derive(Debug)] -pub enum Effect { +pub enum Effect<'a> { /// Some condition which affects which effects might be executed. If the /// condition evaluates to some compile-time constant, we can use that /// to determine which effects are executed and remove the others. Conditional { - condition: Box, - kind: Box, + condition: BumpBox<'a, JsValue<'a>>, + kind: Box>, /// The ast path to the condition. ast_path: Vec, span: Span, }, /// A function call or a new call of a function. Call { - func: Box, - args: Vec, + func: BumpBox<'a, JsValue<'a>>, + args: Vec>, ast_path: Vec, span: Span, in_try: bool, @@ -132,9 +136,9 @@ pub enum Effect { }, /// A function call or a new call of a property of an object. MemberCall { - obj: Box, - prop: Box, - args: Vec, + obj: BumpBox<'a, JsValue<'a>>, + prop: BumpBox<'a, JsValue<'a>>, + args: Vec>, ast_path: Vec, span: Span, in_try: bool, @@ -142,8 +146,8 @@ pub enum Effect { }, /// A property access. Member { - obj: Box, - prop: Box, + obj: BumpBox<'a, JsValue<'a>>, + prop: BumpBox<'a, JsValue<'a>>, ast_path: Vec, span: Span, }, @@ -162,7 +166,7 @@ pub enum Effect { }, /// A typeof expression TypeOf { - arg: Box, + arg: BumpBox<'a, JsValue<'a>>, ast_path: Vec, span: Span, }, @@ -182,7 +186,7 @@ pub enum Effect { /// - `import(/* webpackExports: ["a"] */ './lib')` (magic comment) /// - `import(/* turbopackExports: ["a"] */ './lib')` (magic comment) DynamicImport { - args: Vec, + args: Vec>, ast_path: Vec, span: Span, in_try: bool, @@ -193,43 +197,43 @@ pub enum Effect { Unreachable { start_ast_path: Vec }, } -impl Effect { +impl<'a> Effect<'a> { /// Normalizes all contained values. - pub fn normalize(&mut self) { + pub fn normalize(&mut self, arena: &'a Bump) { match self { Effect::Conditional { condition, kind, .. } => { - condition.normalize(); - kind.normalize(); + condition.normalize(arena); + kind.normalize(arena); } Effect::Call { func, args, .. } => { - func.normalize(); + func.normalize(arena); for arg in args.iter_mut() { - arg.normalize(); + arg.normalize(arena); } } Effect::MemberCall { obj, prop, args, .. } => { - obj.normalize(); - prop.normalize(); + obj.normalize(arena); + prop.normalize(arena); for arg in args.iter_mut() { - arg.normalize(); + arg.normalize(arena); } } Effect::Member { obj, prop, .. } => { - obj.normalize(); - prop.normalize(); + obj.normalize(arena); + prop.normalize(arena); } Effect::DynamicImport { args, .. } => { for arg in args.iter_mut() { - arg.normalize(); + arg.normalize(arena); } } Effect::ImportedBinding { .. } => {} Effect::TypeOf { arg, .. } => { - arg.normalize(); + arg.normalize(arena); } Effect::FreeVar { .. } => {} Effect::ImportMeta { .. } => {} diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs index 757493cfc931..5b3be941f6a9 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs @@ -1,4 +1,4 @@ -use std::{iter, sync::Arc}; +use std::sync::Arc; use anyhow::{Ok, Result}; use rustc_hash::FxHashSet; @@ -12,8 +12,8 @@ use turbo_rcstr::{RcStr, rcstr}; use crate::{ SpecifiedModuleType, analyzer::{ - ConstantNumber, ConstantValue, ImportMap, JsValue, ObjectPart, WellKnownObjectKind, - is_unresolved, + Bump, BumpVec, ConstantNumber, ConstantValue, ImportMap, JsValue, ObjectPart, + WellKnownObjectKind, is_unresolved, }, references::constant_value::parse_single_expr_lit, utils::unparen, @@ -59,25 +59,29 @@ impl EvalContext { self.imports.is_esm(specified_type) } - pub(super) fn eval_prop_name(&self, prop: &PropName) -> JsValue { + pub(super) fn eval_prop_name<'a>(&self, arena: &'a Bump, prop: &PropName) -> JsValue<'a> { match prop { PropName::Ident(ident) => ident.sym.clone().into(), PropName::Str(str) => str.value.clone().to_atom_lossy().into_owned().into(), PropName::Num(num) => num.value.into(), - PropName::Computed(ComputedPropName { expr, .. }) => self.eval(expr), + PropName::Computed(ComputedPropName { expr, .. }) => self.eval(arena, expr), PropName::BigInt(bigint) => (*bigint.value.clone()).into(), } } - pub(super) fn eval_member_prop(&self, prop: &MemberProp) -> Option { + pub(super) fn eval_member_prop<'a>( + &self, + arena: &'a Bump, + prop: &MemberProp, + ) -> Option> { match prop { MemberProp::Ident(ident) => Some(ident.sym.clone().into()), - MemberProp::Computed(ComputedPropName { expr, .. }) => Some(self.eval(expr)), + MemberProp::Computed(ComputedPropName { expr, .. }) => Some(self.eval(arena, expr)), MemberProp::PrivateName(_) => None, } } - fn eval_tpl(&self, e: &Tpl, raw: bool) -> JsValue { + fn eval_tpl<'a>(&self, arena: &'a Bump, e: &Tpl, raw: bool) -> JsValue<'a> { debug_assert!(e.quasis.len() == e.exprs.len() + 1); let mut values = vec![]; @@ -107,20 +111,20 @@ impl EvalContext { let idx = idx / 2; let e = &e.exprs[idx]; - values.push(self.eval(e)); + values.push(self.eval(arena, e)); } } match values.len() { 0 => JsValue::Constant(ConstantValue::Str(rcstr!("").into())), 1 => values.into_iter().next().unwrap(), - _ => JsValue::concat(values), + _ => JsValue::concat(BumpVec::from_iter_in(arena, values)), } } - pub(super) fn eval_ident(&self, i: &Ident) -> JsValue { + pub(super) fn eval_ident<'a>(&self, arena: &'a Bump, i: &Ident) -> JsValue<'a> { let id = i.to_id(); - if let Some(imported) = self.imports.get_import(&id) { + if let Some(imported) = self.imports.get_import(arena, &id) { return imported; } if is_unresolved(i, self.unresolved_mark) || self.force_free_values.contains(&id) { @@ -137,15 +141,15 @@ impl EvalContext { } } - pub fn eval(&self, e: &Expr) -> JsValue { + pub fn eval<'a>(&self, arena: &'a Bump, e: &Expr) -> JsValue<'a> { debug_assert!( GLOBALS.is_set(), "Eval requires globals from its parsed result" ); match e { - Expr::Paren(e) => self.eval(&e.expr), + Expr::Paren(e) => self.eval(arena, &e.expr), Expr::Lit(e) => JsValue::Constant(e.clone().into()), - Expr::Ident(i) => self.eval_ident(i), + Expr::Ident(i) => self.eval_ident(arena, i), Expr::Unary(UnaryExpr { op: op!("void"), @@ -165,9 +169,9 @@ impl EvalContext { Expr::Unary(UnaryExpr { op: op!("!"), arg, .. }) => { - let arg = self.eval(arg); + let arg = self.eval(arena, arg); - JsValue::logical_not(Box::new(arg)) + JsValue::logical_not(arena, arg) } Expr::Unary(UnaryExpr { @@ -175,9 +179,9 @@ impl EvalContext { arg, .. }) => { - let arg = self.eval(arg); + let arg = self.eval(arena, arg); - JsValue::type_of(Box::new(arg)) + JsValue::type_of(arena, arg) } Expr::Bin(BinExpr { @@ -186,15 +190,16 @@ impl EvalContext { right, .. }) => { - let l = self.eval(left); - let r = self.eval(right); + let l = self.eval(arena, left); + let r = self.eval(arena, right); match (l, r) { - (JsValue::Add(c, l), r) => JsValue::Add( - c + r.total_nodes(), - l.into_iter().chain(iter::once(r)).collect(), - ), - (l, r) => JsValue::add(vec![l, r]), + (JsValue::Add(c, mut l), r) => { + let total = c + r.total_nodes(); + l.push(arena, r); + JsValue::Add(total, l) + } + (l, r) => JsValue::add(BumpVec::from_iter_in(arena, [l, r])), } } @@ -203,49 +208,58 @@ impl EvalContext { left, right, .. - }) => JsValue::logical_and(vec![self.eval(left), self.eval(right)]), + }) => JsValue::logical_and(BumpVec::from_iter_in( + arena, + [self.eval(arena, left), self.eval(arena, right)], + )), Expr::Bin(BinExpr { op: op!("||"), left, right, .. - }) => JsValue::logical_or(vec![self.eval(left), self.eval(right)]), + }) => JsValue::logical_or(BumpVec::from_iter_in( + arena, + [self.eval(arena, left), self.eval(arena, right)], + )), Expr::Bin(BinExpr { op: op!("??"), left, right, .. - }) => JsValue::nullish_coalescing(vec![self.eval(left), self.eval(right)]), + }) => JsValue::nullish_coalescing(BumpVec::from_iter_in( + arena, + [self.eval(arena, left), self.eval(arena, right)], + )), Expr::Bin(BinExpr { op: op!("=="), left, right, .. - }) => JsValue::equal(Box::new(self.eval(left)), Box::new(self.eval(right))), + }) => JsValue::equal(arena, self.eval(arena, left), self.eval(arena, right)), Expr::Bin(BinExpr { op: op!("!="), left, right, .. - }) => JsValue::not_equal(Box::new(self.eval(left)), Box::new(self.eval(right))), + }) => JsValue::not_equal(arena, self.eval(arena, left), self.eval(arena, right)), Expr::Bin(BinExpr { op: op!("==="), left, right, .. - }) => JsValue::strict_equal(Box::new(self.eval(left)), Box::new(self.eval(right))), + }) => JsValue::strict_equal(arena, self.eval(arena, left), self.eval(arena, right)), Expr::Bin(BinExpr { op: op!("!=="), left, right, .. - }) => JsValue::strict_not_equal(Box::new(self.eval(left)), Box::new(self.eval(right))), + }) => JsValue::strict_not_equal(arena, self.eval(arena, left), self.eval(arena, right)), &Expr::Cond(CondExpr { box ref cons, @@ -253,23 +267,19 @@ impl EvalContext { box ref test, .. }) => { - let test = self.eval(test); + let test = self.eval(arena, test); if let Some(truthy) = test.is_truthy() { if truthy { - self.eval(cons) + self.eval(arena, cons) } else { - self.eval(alt) + self.eval(arena, alt) } } else { - JsValue::tenary( - Box::new(test), - Box::new(self.eval(cons)), - Box::new(self.eval(alt)), - ) + JsValue::tenary(arena, test, self.eval(arena, cons), self.eval(arena, alt)) } } - Expr::Tpl(e) => self.eval_tpl(e, false), + Expr::Tpl(e) => self.eval_tpl(arena, e, false), Expr::TaggedTpl(TaggedTpl { tag: @@ -285,7 +295,7 @@ impl EvalContext { && &*tag_prop.sym == "raw" && is_unresolved(tag_obj, self.unresolved_mark) { - self.eval_tpl(tpl, true) + self.eval_tpl(arena, tpl, true) } else { JsValue::unknown_empty( true, @@ -309,10 +319,10 @@ impl EvalContext { SyntaxContext::empty(), )), - Expr::Await(AwaitExpr { arg, .. }) => JsValue::awaited(Box::new(self.eval(arg))), + Expr::Await(AwaitExpr { arg, .. }) => JsValue::awaited(arena, self.eval(arena, arg)), Expr::Seq(e) => { - let mut seq = e.exprs.iter().map(|e| self.eval(e)).peekable(); + let mut seq = e.exprs.iter().map(|e| self.eval(arena, e)).peekable(); let mut side_effects = false; let mut last = seq.next().unwrap(); for e in seq { @@ -330,8 +340,8 @@ impl EvalContext { prop: MemberProp::Ident(prop), .. }) => { - let obj = self.eval(obj); - JsValue::member(Box::new(obj), Box::new(prop.sym.clone().into())) + let obj = self.eval(arena, obj); + JsValue::member(arena, obj, prop.sym.clone().into()) } Expr::Member(MemberExpr { @@ -339,9 +349,9 @@ impl EvalContext { prop: MemberProp::Computed(computed), .. }) => { - let obj = self.eval(obj); - let prop = self.eval(&computed.expr); - JsValue::member(Box::new(obj), Box::new(prop)) + let obj = self.eval(arena, obj); + let prop = self.eval(arena, &computed.expr); + JsValue::member(arena, obj, prop) } Expr::New(NewExpr { @@ -359,8 +369,9 @@ impl EvalContext { } JsValue::new_from_iter( - self.eval(callee), - args.iter().map(|arg| self.eval(&arg.expr)), + arena, + self.eval(arena, callee), + args.iter().map(|arg| self.eval(arena, &arg.expr)), ) } @@ -386,18 +397,22 @@ impl EvalContext { rcstr!("private names in function calls is not supported"), ); } - MemberProp::Computed(ComputedPropName { expr, .. }) => self.eval(expr), + MemberProp::Computed(ComputedPropName { expr, .. }) => { + self.eval(arena, expr) + } }; - let obj = self.eval(obj); + let obj = self.eval(arena, obj); JsValue::member_call_from_iter( + arena, obj, prop, - args.iter().map(|arg| self.eval(&arg.expr)), + args.iter().map(|arg| self.eval(arena, &arg.expr)), ) } else { JsValue::call_from_iter( - self.eval(callee), - args.iter().map(|arg| self.eval(&arg.expr)), + arena, + self.eval(arena, callee), + args.iter().map(|arg| self.eval(arena, &arg.expr)), ) } } @@ -415,7 +430,11 @@ impl EvalContext { ); } - let args = args.iter().map(|arg| self.eval(&arg.expr)).collect(); + let args = bumpalo::collections::Vec::from_iter_in( + args.iter().map(|arg| self.eval(arena, &arg.expr)), + arena, + ) + .into_boxed_slice(); JsValue::super_call(args) } @@ -433,8 +452,9 @@ impl EvalContext { ); } JsValue::call_from_iter( + arena, JsValue::FreeVar(atom!("import")), - args.iter().map(|arg| self.eval(&arg.expr)), + args.iter().map(|arg| self.eval(arena, &arg.expr)), ) } @@ -443,38 +463,38 @@ impl EvalContext { return JsValue::unknown_empty(true, rcstr!("spread is not supported")); } - let arr = arr - .elems - .iter() - .map(|e| match e { - Some(e) => self.eval(&e.expr), + let arr = BumpVec::from_iter_in( + arena, + arr.elems.iter().map(|e| match e { + Some(e) => self.eval(arena, &e.expr), _ => JsValue::Constant(ConstantValue::Undefined), - }) - .collect(); + }), + ); JsValue::array(arr) } - Expr::Object(obj) => JsValue::object( - obj.props - .iter() - .map(|prop| match prop { - PropOrSpread::Spread(SpreadElement { expr, .. }) => { - ObjectPart::Spread(self.eval(expr)) - } - PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp { key, box value })) => { - ObjectPart::KeyValue(self.eval_prop_name(key), self.eval(value)) - } - PropOrSpread::Prop(box Prop::Shorthand(ident)) => ObjectPart::KeyValue( - ident.sym.clone().into(), - self.eval(&Expr::Ident(ident.clone())), - ), - _ => ObjectPart::Spread(JsValue::unknown_empty( - true, - rcstr!("unsupported object part"), - )), - }) - .collect(), - ), + Expr::Object(obj) => JsValue::object(BumpVec::from_iter_in( + arena, + obj.props.iter().map(|prop| match prop { + PropOrSpread::Spread(SpreadElement { expr, .. }) => { + ObjectPart::Spread(self.eval(arena, expr)) + } + PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp { key, box value })) => { + ObjectPart::KeyValue( + self.eval_prop_name(arena, key), + self.eval(arena, value), + ) + } + PropOrSpread::Prop(box Prop::Shorthand(ident)) => ObjectPart::KeyValue( + ident.sym.clone().into(), + self.eval(arena, &Expr::Ident(ident.clone())), + ), + _ => ObjectPart::Spread(JsValue::unknown_empty( + true, + rcstr!("unsupported object part"), + )), + }), + )), Expr::MetaProp(MetaPropExpr { kind: MetaPropKind::ImportMeta, @@ -482,8 +502,8 @@ impl EvalContext { }) => JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), Expr::Assign(AssignExpr { op, .. }) => match op { - // TODO: `self.eval(right)` would be the value, but we need to handle the side - // effect of that expression + // TODO: `self.eval(arena, right)` would be the value, but we need to handle the + // side effect of that expression AssignOp::Assign => JsValue::unknown_empty(true, rcstr!("assignment expression")), _ => JsValue::unknown_empty(true, rcstr!("compound assignment expression")), }, @@ -492,7 +512,7 @@ impl EvalContext { } } - pub fn eval_single_expr_lit(expr_lit: &RcStr) -> Result { + pub fn eval_single_expr_lit<'a>(arena: &'a Bump, expr_lit: &RcStr) -> Result> { let cm = Lrc::new(SourceMap::default()); let js_value = try_with_handler(cm, Default::default(), |_| { @@ -501,7 +521,7 @@ impl EvalContext { let eval_context = EvalContext::new(None, Mark::new(), Mark::new(), Default::default(), None); - Ok(eval_context.eval(&expr)) + Ok(eval_context.eval(arena, &expr)) }) }) .map_err(|e| e.to_pretty_error())?; diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs index e5b356482cf3..e9673c4489a8 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs @@ -12,7 +12,7 @@ pub use crate::analyzer::graph::{ }; use crate::{ AnalyzeMode, - analyzer::{JsValue, graph::visitor::Analyzer}, + analyzer::{Bump, JsValue, graph::visitor::Analyzer}, code_gen::CodeGen, }; @@ -21,8 +21,8 @@ mod eval_context; mod visitor; #[derive(Debug)] -pub struct VarGraph { - pub values: FxHashMap, +pub struct VarGraph<'a> { + pub values: FxHashMap>, /// Map [`JsValue::FreeVar`] names to their [`Id`] to facilitate lookups into [`Self::values`]. /// @@ -30,50 +30,50 @@ pub struct VarGraph { /// non-trivial values. pub free_var_ids: FxHashMap, - pub effects: Vec, + pub effects: Vec>, // Some unconditional codegens, usually for ESM items. pub code_gens: Vec, } -impl VarGraph { - pub fn normalize(&mut self) { +impl<'a> VarGraph<'a> { + pub fn normalize(&mut self, arena: &'a Bump) { for value in self.values.values_mut() { - value.normalize(); + value.normalize(arena); } for effect in self.effects.iter_mut() { - effect.normalize(); + effect.normalize(arena); } } } -pub fn create_graph( +pub fn create_graph<'a>( + arena: &'a Bump, m: &Program, eval_context: &EvalContext, analyze_mode: AnalyzeMode, supports_block_scoping: bool, -) -> VarGraph { - let mut graph = VarGraph { - values: Default::default(), - free_var_ids: Default::default(), +) -> VarGraph<'a> { + let mut analyzer = Analyzer { + arena, + analyze_mode, + data: VarGraph { + values: Default::default(), + free_var_ids: Default::default(), + effects: Default::default(), + code_gens: Default::default(), + }, + eval_context, + state: Default::default(), effects: Default::default(), + hoisted_effects: Default::default(), code_gens: Default::default(), + supports_block_scoping, }; - m.visit_with_ast_path( - &mut Analyzer { - analyze_mode, - data: &mut graph, - eval_context, - state: Default::default(), - effects: Default::default(), - hoisted_effects: Default::default(), - code_gens: Default::default(), - supports_block_scoping, - }, - &mut Default::default(), - ); + m.visit_with_ast_path(&mut analyzer, &mut Default::default()); - graph.normalize(); + let mut graph = analyzer.data; + graph.normalize(arena); graph } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs index 83a094a060ea..4d1683cbb340 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs @@ -3,6 +3,7 @@ use std::{ mem::{replace, take}, }; +use bumpalo::boxed::Box as BumpBox; use smallvec::SmallVec; use swc_core::{ common::{Span, Spanned, SyntaxContext, pass::AstNodePath}, @@ -19,7 +20,7 @@ use turbopack_core::resolve::ExportUsage; use crate::{ AnalyzeMode, analyzer::{ - ConstantValue, JsValue, WellKnownFunctionKind, + Bump, BumpVec, ConstantValue, JsValue, WellKnownFunctionKind, graph::{ConditionalKind, Effect, EffectArg, EffectsBlock, EvalContext, VarGraph}, is_unresolved_id, }, @@ -28,18 +29,18 @@ use crate::{ utils::{AstPathRange, unparen}, }; -enum EarlyReturn { +enum EarlyReturn<'a> { Always { - prev_effects: Vec, + prev_effects: Vec>, start_ast_path: Vec, }, Conditional { - prev_effects: Vec, + prev_effects: Vec>, start_ast_path: Vec, - condition: Box, - then: Option>, - r#else: Option>, + condition: BumpBox<'a, JsValue<'a>>, + then: Option>>, + r#else: Option>>, /// The ast path to the condition. condition_ast_path: Vec, span: Span, @@ -56,17 +57,19 @@ pub fn as_parent_path_skip( kinds[..kinds.len() - skip].to_vec() } -pub(super) struct Analyzer<'a> { +pub(super) struct Analyzer<'arena, 'eval> { + pub(super) arena: &'arena Bump, + pub(super) analyze_mode: AnalyzeMode, - pub(super) data: &'a mut VarGraph, - pub(super) state: analyzer_state::AnalyzerState, + pub(super) data: VarGraph<'arena>, + pub(super) state: analyzer_state::AnalyzerState<'arena>, - pub(super) effects: Vec, + pub(super) effects: Vec>, /// Effects collected from hoisted declarations. See https://developer.mozilla.org/en-US/docs/Glossary/Hoisting /// Tracked separately so we can preserve effects from hoisted declarations even when we don't /// collect effects from the declaring context. - pub(super) hoisted_effects: Vec, + pub(super) hoisted_effects: Vec>, // Some unconditional codegens, usually for ESM items. pub(super) code_gens: Vec, @@ -75,7 +78,7 @@ pub(super) struct Analyzer<'a> { /// slightly less correct circular import errors) for EsmModuleItem pub(super) supports_block_scoping: bool, - pub(super) eval_context: &'a EvalContext, + pub(super) eval_context: &'eval EvalContext, } trait FunctionLike { @@ -154,20 +157,20 @@ mod analyzer_state { /// Contains fields of `Analyzer` that should only be modified using helper methods. These are /// intentionally private to the rest of the `Analyzer` implementation. #[derive(Default)] - pub struct AnalyzerState { - pat_value: Option, + pub struct AnalyzerState<'a> { + pat_value: Option>, /// Return values of the current function. /// /// This is configured to [Some] by function handlers and filled by the /// return statement handler. - cur_fn_return_values: Option>, + cur_fn_return_values: Option>>, /// Stack of early returns for control flow analysis. - early_return_stack: Vec, + early_return_stack: Vec>, lexical_stack: Vec, var_decl_kind: Option, } - impl Analyzer<'_> { + impl<'a> Analyzer<'a, '_> { /// Returns true if we are in a function. False if we are in the root scope. pub(super) fn is_in_fn(&self) -> bool { self.state @@ -237,7 +240,7 @@ mod analyzer_state { /// Adds a return value to the current function. /// Panics if we are not in a function scope - pub(super) fn add_return_value(&mut self, value: JsValue) { + pub(super) fn add_return_value(&mut self, value: JsValue<'a>) { self.state .cur_fn_return_values .as_mut() @@ -251,7 +254,7 @@ mod analyzer_state { /// /// Consumes the value, setting it to `None`, and returning the previous value. This avoids /// extra clones. - pub(super) fn take_pat_value(&mut self) -> Option { + pub(super) fn take_pat_value(&mut self) -> Option> { self.state.pat_value.take() } @@ -260,7 +263,7 @@ mod analyzer_state { // `None`) afterwards. pub(super) fn with_pat_value( &mut self, - value: Option, + value: Option>, func: impl FnOnce(&mut Self) -> T, ) -> T { let prev_value = replace(&mut self.state.pat_value, value); @@ -293,7 +296,8 @@ mod analyzer_state { &mut self, function: &impl FunctionLike, visitor: impl FnOnce(&mut Self), - ) -> JsValue { + ) -> JsValue<'a> { + let arena = self.arena; let fn_id = function.span().lo.0; let prev_return_values = self.state.cur_fn_return_values.replace(vec![]); @@ -308,19 +312,20 @@ mod analyzer_state { self.state.cur_fn_return_values = prev_return_values; JsValue::function( + arena, fn_id, function.is_async(), function.is_generator(), match return_values.len() { 0 => JsValue::Constant(ConstantValue::Undefined), 1 => return_values.into_iter().next().unwrap(), - _ => JsValue::alternatives(return_values), + _ => JsValue::alternatives(BumpVec::from_iter_in(arena, return_values)), }, ) } /// Helper to access the early_return_stack mutably (for push operations) - pub(super) fn early_return_stack_mut(&mut self) -> &mut Vec { + pub(super) fn early_return_stack_mut(&mut self) -> &mut Vec> { &mut self.state.early_return_stack } @@ -624,14 +629,14 @@ impl CallOrNewExpr<'_> { } } -impl Analyzer<'_> { - fn add_value(&mut self, id: Id, value: JsValue) { +impl<'a> Analyzer<'a, '_> { + fn add_value(&mut self, id: Id, value: JsValue<'a>) { if is_unresolved_id(&id, self.eval_context.unresolved_mark) { self.data.free_var_ids.insert(id.0.clone(), id.clone()); } if let Some(prev) = self.data.values.get_mut(&id) { - prev.add_alt(value); + prev.add_alt(self.arena, value); } else { self.data.values.insert(id, value); } @@ -641,12 +646,12 @@ impl Analyzer<'_> { } fn add_value_from_expr(&mut self, id: Id, value: &Expr) { - let value = self.eval_context.eval(value); + let value = self.eval_context.eval(self.arena, value); self.add_value(id, value); } - fn add_effect(&mut self, effect: Effect) { + fn add_effect(&mut self, effect: Effect<'a>) { self.effects.push(effect); } @@ -767,7 +772,9 @@ impl Analyzer<'_> { arrow_expr, ArrowExprField::Params(i), )); - let pat_value = iter.next().map(|arg| self.eval_context.eval(&arg.expr)); + let pat_value = iter + .next() + .map(|arg| self.eval_context.eval(self.arena, &arg.expr)); self.with_pat_value(pat_value, |this| this.visit_pat(param, &mut ast_path)); } { @@ -819,9 +826,10 @@ impl Analyzer<'_> { FunctionField::Params(i), )); if let Some(arg) = iter.next() { - self.with_pat_value(Some(self.eval_context.eval(&arg.expr)), |this| { - this.visit_param(param, &mut ast_path) - }); + self.with_pat_value( + Some(self.eval_context.eval(self.arena, &arg.expr)), + |this| this.visit_param(param, &mut ast_path), + ); } else { self.visit_param(param, &mut ast_path); } @@ -879,7 +887,7 @@ impl Analyzer<'_> { CallOrNewExpr::New(n) => AstParentNodeRef::NewExpr(n, NewExprField::Args(i)), }); if arg.spread.is_none() { - let value = self.eval_context.eval(&arg.expr); + let value = self.eval_context.eval(self.arena, &arg.expr); let block_path = match &*arg.expr { Expr::Fn(FnExpr { .. }) => { @@ -962,16 +970,20 @@ impl Analyzer<'_> { } Callee::Expr(box expr) => { if let Expr::Member(MemberExpr { obj, prop, .. }) = unparen(expr) { - let obj_value = Box::new(self.eval_context.eval(obj)); + let obj_value = + BumpBox::new_in(self.eval_context.eval(self.arena, obj), self.arena); let prop_value = match prop { // TODO avoid clone - MemberProp::Ident(i) => Box::new(i.sym.clone().into()), - MemberProp::PrivateName(_) => Box::new(JsValue::unknown_empty( - false, - rcstr!("private names in member expressions are not supported"), - )), + MemberProp::Ident(i) => BumpBox::new_in(i.sym.clone().into(), self.arena), + MemberProp::PrivateName(_) => BumpBox::new_in( + JsValue::unknown_empty( + false, + rcstr!("private names in member expressions are not supported"), + ), + self.arena, + ), MemberProp::Computed(ComputedPropName { expr, .. }) => { - Box::new(self.eval_context.eval(expr)) + BumpBox::new_in(self.eval_context.eval(self.arena, expr), self.arena) } }; self.add_effect(Effect::MemberCall { @@ -984,7 +996,8 @@ impl Analyzer<'_> { new, }); } else { - let fn_value = Box::new(self.eval_context.eval(expr)); + let fn_value = + BumpBox::new_in(self.eval_context.eval(self.arena, expr), self.arena); self.add_effect(Effect::Call { func: fn_value, args, @@ -996,10 +1009,11 @@ impl Analyzer<'_> { } } Callee::Super(_) => self.add_effect(Effect::Call { - func: Box::new( + func: BumpBox::new_in( self.eval_context // Unwrap because `new super(..)` isn't valid anyway - .eval(&Expr::Call(n.as_call().unwrap().clone())), + .eval(self.arena, &Expr::Call(n.as_call().unwrap().clone())), + self.arena, ), args, ast_path: as_parent_path(ast_path), @@ -1019,15 +1033,18 @@ impl Analyzer<'_> { return; } - let obj_value = Box::new(self.eval_context.eval(&member_expr.obj)); + let obj_value = BumpBox::new_in( + self.eval_context.eval(self.arena, &member_expr.obj), + self.arena, + ); let prop_value = match &member_expr.prop { // TODO avoid clone - MemberProp::Ident(i) => Box::new(i.sym.clone().into()), + MemberProp::Ident(i) => BumpBox::new_in(i.sym.clone().into(), self.arena), MemberProp::PrivateName(_) => { return; } MemberProp::Computed(ComputedPropName { expr, .. }) => { - Box::new(self.eval_context.eval(expr)) + BumpBox::new_in(self.eval_context.eval(self.arena, expr), self.arena) } }; self.add_effect(Effect::Member { @@ -1048,7 +1065,7 @@ impl Analyzer<'_> { } } -impl VisitAstPath for Analyzer<'_> { +impl VisitAstPath for Analyzer<'_, '_> { fn visit_import_decl<'ast: 'r, 'r>( &mut self, import: &'ast ImportDecl, @@ -1080,15 +1097,17 @@ impl VisitAstPath for Analyzer<'_> { ast_path.with_guard(AstParentNodeRef::AssignExpr(n, AssignExprField::Left)); let pat_value = match (n.op, n.left.as_ident()) { - (AssignOp::Assign, _) => self.eval_context.eval(&n.right), + (AssignOp::Assign, _) => self.eval_context.eval(self.arena, &n.right), (AssignOp::AndAssign | AssignOp::OrAssign | AssignOp::NullishAssign, Some(_)) => { // We can handle the right value as alternative to the existing value - self.eval_context.eval(&n.right) + self.eval_context.eval(self.arena, &n.right) } (AssignOp::AddAssign, Some(key)) => { - let left = self.eval_context.eval(&Expr::Ident(key.clone().into())); - let right = self.eval_context.eval(&n.right); - JsValue::add(vec![left, right]) + let left = self + .eval_context + .eval(self.arena, &Expr::Ident(key.clone().into())); + let right = self.eval_context.eval(self.arena, &n.right); + JsValue::add(BumpVec::from_iter_in(self.arena, [left, right])) } _ => JsValue::unknown_empty(true, rcstr!("unsupported assign operation")), }; @@ -1308,7 +1327,7 @@ impl VisitAstPath for Analyzer<'_> { expr.body.visit_with_ast_path(this, &mut ast_path); // If body is a single expression treat it as a Block with an return statement if let BlockStmtOrExpr::Expr(inner_expr) = &*expr.body { - let implicit_return_value = this.eval_context.eval(inner_expr); + let implicit_return_value = this.eval_context.eval(this.arena, inner_expr); this.add_return_value(implicit_return_value); } } @@ -1442,12 +1461,12 @@ impl VisitAstPath for Analyzer<'_> { let should_include_undefined = var_decl_kind == VarDeclKind::Var && self.is_in_nested_block_scope(); - let init_value = self.eval_context.eval(init); + let init_value = self.eval_context.eval(self.arena, init); let pat_value = Some(if should_include_undefined { - JsValue::alternatives(vec![ - init_value, - JsValue::Constant(ConstantValue::Undefined), - ]) + JsValue::alternatives(BumpVec::from_iter_in( + self.arena, + [init_value, JsValue::Constant(ConstantValue::Undefined)], + )) } else { init_value }); @@ -1487,7 +1506,8 @@ impl VisitAstPath for Analyzer<'_> { ast_path.with_guard(AstParentNodeRef::ForInStmt(n, ForInStmtField::Left)); self.with_pat_value( // TODO this should really be - // `Some(JsValue::iteratedKeys(Box::new(self.eval_context.eval(&n.right))))` + // `Some(JsValue::iteratedKeys(Box::new(self.eval_context.eval(self.arena, + // &n.right))))` Some(JsValue::unknown_empty( false, rcstr!("for-in variable currently not analyzed"), @@ -1517,10 +1537,10 @@ impl VisitAstPath for Analyzer<'_> { n.right.visit_with_ast_path(self, &mut ast_path); } - let iterable = self.eval_context.eval(&n.right); + let iterable = self.eval_context.eval(self.arena, &n.right); // TODO n.await is ignored (async interables) - self.with_pat_value(Some(JsValue::iterated(Box::new(iterable))), |this| { + self.with_pat_value(Some(JsValue::iterated(self.arena, iterable)), |this| { let mut ast_path = ast_path.with_guard(AstParentNodeRef::ForOfStmt(n, ForOfStmtField::Left)); n.left.visit_with_ast_path(this, &mut ast_path); @@ -1709,7 +1729,7 @@ impl VisitAstPath for Analyzer<'_> { let return_value = stmt .arg .as_deref() - .map(|e| self.eval_context.eval(e)) + .map(|e| self.eval_context.eval(self.arena, e)) .unwrap_or(JsValue::Constant(ConstantValue::Undefined)); self.add_return_value(return_value); @@ -1745,7 +1765,7 @@ impl VisitAstPath for Analyzer<'_> { .should_import_all(esm_reference_index) && let Some(AstParentNodeRef::MemberExpr(member, MemberExprField::Obj)) = ast_path.get(ast_path.len() - 2) - && let Some(prop) = self.eval_context.eval_member_prop(&member.prop) + && let Some(prop) = self.eval_context.eval_member_prop(self.arena, &member.prop) && let Some(prop_str) = prop.as_str() { // a namespace member access like @@ -1770,7 +1790,7 @@ impl VisitAstPath for Analyzer<'_> { // If this identifier is free, produce an effect so we can potentially replace it later. if self.analyze_mode.is_code_gen() - && let JsValue::FreeVar(var) = self.eval_context.eval_ident(ident) + && let JsValue::FreeVar(var) = self.eval_context.eval_ident(self.arena, ident) { // TODO(lukesandberg): we should consider filtering effects here, e.g. there is no // benefit in an Effect for `window` or `Math` @@ -2036,7 +2056,7 @@ impl VisitAstPath for Analyzer<'_> { ast_path: &mut swc_core::ecma::visit::AstNodePath<'r>, ) { if n.op == UnaryOp::TypeOf && self.analyze_mode.is_code_gen() { - let arg_value = Box::new(self.eval_context.eval(&n.arg)); + let arg_value = BumpBox::new_in(self.eval_context.eval(self.arena, &n.arg), self.arena); self.add_effect(Effect::TypeOf { arg: arg_value, @@ -2061,7 +2081,10 @@ impl VisitAstPath for Analyzer<'_> { let effects = take(&mut self.effects); prev_effects.push(Effect::Conditional { - condition: Box::new(JsValue::unknown_empty(true, rcstr!("labeled statement"))), + condition: BumpBox::new_in( + JsValue::unknown_empty(true, rcstr!("labeled statement")), + self.arena, + ), kind: Box::new(ConditionalKind::Labeled { body: Box::new(EffectsBlock { effects, @@ -2141,15 +2164,15 @@ impl VisitAstPath for Analyzer<'_> { } } -impl Analyzer<'_> { +impl<'a> Analyzer<'a, '_> { fn add_conditional_if_effect_with_early_return( &mut self, test: &Expr, ast_path: &AstNodePath>, condition_ast_kind: AstParentKind, span: Span, - then: Option>, - r#else: Option>, + then: Option>>, + r#else: Option>>, early_return_when_true: bool, early_return_when_false: bool, ) { @@ -2157,7 +2180,7 @@ impl Analyzer<'_> { { return; } - let condition = Box::new(self.eval_context.eval(test)); + let condition = BumpBox::new_in(self.eval_context.eval(self.arena, test), self.arena); if condition.is_unknown() { if let Some(mut then) = then { self.effects.append(&mut then.effects); @@ -2227,9 +2250,9 @@ impl Analyzer<'_> { ast_path: &AstNodePath>, ast_kind: AstParentKind, span: Span, - mut cond_kind: ConditionalKind, + mut cond_kind: ConditionalKind<'a>, ) { - let condition = Box::new(self.eval_context.eval(test)); + let condition = BumpBox::new_in(self.eval_context.eval(self.arena, test), self.arena); if condition.is_unknown() { match &mut cond_kind { ConditionalKind::If { then } => { @@ -2273,7 +2296,7 @@ impl Analyzer<'_> { fn handle_array_pat_with_value<'ast: 'r, 'r>( &mut self, arr: &'ast ArrayPat, - pat_value: JsValue, + pat_value: JsValue<'a>, ast_path: &mut AstNodePath>, ) { match pat_value { @@ -2283,7 +2306,12 @@ impl Analyzer<'_> { .iter() // TODO: This does not handle inline spreads correctly // e.g. `let [a,..b,c] = [1,2,3]` - .zip(items.into_iter().map(Some).chain(iter::repeat(None))) + .zip( + items + .into_iter() + .map(Some) + .chain(iter::repeat_with(|| None)), + ) .enumerate() { self.with_pat_value(value_item, |this| { @@ -2296,8 +2324,9 @@ impl Analyzer<'_> { value => { for (idx, elem) in arr.elems.iter().enumerate() { let pat_value = Some(JsValue::member( - Box::new(value.clone()), - Box::new(JsValue::Constant(ConstantValue::Num((idx as f64).into()))), + self.arena, + value.clone_in(self.arena), + JsValue::Constant(ConstantValue::Num((idx as f64).into())), )); self.with_pat_value(pat_value, |this| { let mut ast_path = ast_path @@ -2312,7 +2341,7 @@ impl Analyzer<'_> { fn handle_object_pat_with_value<'ast: 'r, 'r>( &mut self, obj: &'ast ObjectPat, - pat_value: JsValue, + pat_value: JsValue<'a>, ast_path: &mut AstNodePath>, ) { for (i, prop) in obj.props.iter().enumerate() { @@ -2325,7 +2354,7 @@ impl Analyzer<'_> { ObjectPatPropField::KeyValue, )); let KeyValuePatProp { key, value } = kv; - let key_value = self.eval_context.eval_prop_name(key); + let key_value = self.eval_context.eval_prop_name(self.arena, key); { let mut ast_path = ast_path.with_guard(AstParentNodeRef::KeyValuePatProp( kv, @@ -2334,8 +2363,9 @@ impl Analyzer<'_> { key.visit_with_ast_path(self, &mut ast_path); } let pat_value = Some(JsValue::member( - Box::new(pat_value.clone()), - Box::new(key_value), + self.arena, + pat_value.clone_in(self.arena), + key_value, )); self.with_pat_value(pat_value, |this| { let mut ast_path = ast_path.with_guard(AstParentNodeRef::KeyValuePatProp( @@ -2362,13 +2392,20 @@ impl Analyzer<'_> { self.add_value( key.to_id(), if let Some(box value) = value { - let value = self.eval_context.eval(value); - JsValue::alternatives(vec![ - JsValue::member(Box::new(pat_value.clone()), Box::new(key_value)), - value, - ]) + let value = self.eval_context.eval(self.arena, value); + JsValue::alternatives(BumpVec::from_iter_in( + self.arena, + [ + JsValue::member( + self.arena, + pat_value.clone_in(self.arena), + key_value, + ), + value, + ], + )) } else { - JsValue::member(Box::new(pat_value.clone()), Box::new(key_value)) + JsValue::member(self.arena, pat_value.clone_in(self.arena), key_value) }, ); { diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs index c1c22519eda1..8d3035e46f8e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs @@ -27,7 +27,7 @@ use super::{JsValue, ModuleValue, top_level_await::has_top_level_await}; use crate::{ SpecifiedModuleType, analyzer::{ - ConstantValue, ObjectPart, + Bump, ConstantValue, ObjectPart, graph::{AssignmentScope, AssignmentScopes, EvalContext}, is_unresolved, }, @@ -160,7 +160,7 @@ impl ImportAnnotations { } } - pub fn parse_dynamic(with: &JsValue) -> Option { + pub fn parse_dynamic(with: &JsValue<'_>) -> Option { let mut map = BTreeMap::new(); let JsValue::Object { parts, .. } = with else { @@ -524,15 +524,16 @@ impl ImportMap { } } - pub fn get_import(&self, id: &Id) -> Option { + pub fn get_import<'a>(&self, arena: &'a Bump, id: &Id) -> Option> { if let Some((i, i_sym)) = self.imports.get(id) { let r = &self.references[*i]; return Some(JsValue::member( - Box::new(JsValue::Module(ModuleValue { + arena, + JsValue::Module(ModuleValue { module: r.module_path.clone(), annotations: r.annotations.clone(), - })), - Box::new(i_sym.clone().into()), + }), + i_sym.clone().into(), )); } if let Some(i) = self.namespace_imports.get(id) { diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/constants.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/constants.rs index 78be86d35e91..f585e078a6e4 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/constants.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/constants.rs @@ -14,22 +14,34 @@ use swc_core::{ use turbo_rcstr::RcStr; use crate::{ - analyzer::{JsValue, imports::ImportAnnotations}, + analyzer::{Bump, JsValue, imports::ImportAnnotations}, utils::StringifyJs, }; -#[derive(Debug, Clone, Hash, PartialEq)] -pub enum ObjectPart { - KeyValue(JsValue, JsValue), - Spread(JsValue), +#[derive(Debug, Hash, PartialEq)] +pub enum ObjectPart<'a> { + KeyValue(JsValue<'a>, JsValue<'a>), + Spread(JsValue<'a>), } -impl Default for ObjectPart { +impl Default for ObjectPart<'_> { fn default() -> Self { ObjectPart::Spread(Default::default()) } } +impl<'a> ObjectPart<'a> { + /// Deep-clone this object part into `arena`. See [`JsValue::clone_in`]. + pub(crate) fn clone_in(&self, arena: &'a Bump) -> Self { + match self { + ObjectPart::KeyValue(k, v) => { + ObjectPart::KeyValue(k.clone_in(arena), v.clone_in(arena)) + } + ObjectPart::Spread(s) => ObjectPart::Spread(s.clone_in(arena)), + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct ConstantNumber(pub f64); diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/display.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/display.rs index 3026681a7930..fadf50d9d84f 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/display.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/display.rs @@ -4,7 +4,7 @@ use either::Either; use crate::analyzer::{JsValue, ModuleValue, ObjectPart}; -impl Display for ObjectPart { +impl Display for ObjectPart<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ObjectPart::KeyValue(key, value) => write!(f, "{key}: {value}"), @@ -13,7 +13,7 @@ impl Display for ObjectPart { } } -impl Display for JsValue { +impl Display for JsValue<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { JsValue::Constant(v) => write!(f, "{v}"), diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/explain.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/explain.rs index 8146bb615292..d0c6097d46c5 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/explain.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/explain.rs @@ -5,8 +5,12 @@ use either::Either; use crate::analyzer::{JsValue, ModuleValue, ObjectPart, jsvalue::pretty_join}; // Methods for explaining a value -impl JsValue { - pub fn explain_args(args: &[JsValue], depth: usize, unknown_depth: usize) -> (String, String) { +impl JsValue<'_> { + pub fn explain_args( + args: &[JsValue<'_>], + depth: usize, + unknown_depth: usize, + ) -> (String, String) { let mut hints = Vec::new(); let args = args .iter() diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/mod.rs index 32a9ad9869e7..6d9fedadc6c6 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/mod.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::Result; +use bumpalo::boxed::Box as BumpBox; use num_bigint::BigInt; use smallvec::SmallVec; use swc_core::ecma::{ast::Id, atoms::Atom}; @@ -15,7 +16,7 @@ use turbopack_core::compile_time_info::{ }; use crate::analyzer::{ - WellKnownFunctionKind, WellKnownObjectKind, + Bump, BumpVec, WellKnownFunctionKind, WellKnownObjectKind, graph::{EvalContext, VarGraph}, }; @@ -31,7 +32,7 @@ use constants::JsValueMetaKind; pub use constants::*; /// Sum of [`JsValue::total_nodes`] across a slice of values. -fn total_nodes(vec: &[JsValue]) -> u32 { +fn total_nodes(vec: &[JsValue<'_>]) -> u32 { vec.iter().map(|v| v.total_nodes()).sum::() } @@ -96,8 +97,8 @@ fn pretty_join( /// - Replace all built-in functions with their values when they are compile-time constant. /// - For optimization, any nested operations are replaced with [JsValue::Unknown]. So only one /// layer of operation remains. Any remaining operation or placeholder can be treated as unknown. -#[derive(Debug, Clone, Hash, PartialEq)] -pub enum JsValue { +#[derive(Debug, Hash, PartialEq)] +pub enum JsValue<'a> { // LEAF VALUES // ---------------------------- /// A constant primitive value. @@ -108,11 +109,11 @@ pub enum JsValue { /// (must not be an array, otherwise Array.concat needs to be changed) WellKnownObject(WellKnownObjectKind), /// Some kind of well-known function - WellKnownFunction(WellKnownFunctionKind), + WellKnownFunction(WellKnownFunctionKind<'a>), /// Not-analyzable value. Might contain the original value for additional /// info. Has a reason string for explanation. Unknown { - original_value: Option>, + original_value: Option>>, reason: RcStr, has_side_effects: bool, }, @@ -122,72 +123,82 @@ pub enum JsValue { /// An array of nested values Array { total_nodes: u32, - items: Vec, + items: BumpVec<'a, JsValue<'a>>, mutable: bool, }, /// An object of nested values Object { total_nodes: u32, - parts: Vec, + parts: BumpVec<'a, ObjectPart<'a>>, mutable: bool, }, /// A list of alternative values Alternatives { total_nodes: u32, - values: Vec, + values: BumpVec<'a, JsValue<'a>>, logical_property: Option, }, /// A function reference. The return value might contain [JsValue::Argument] /// placeholders that need to be replaced when calling this function. /// `(total_node_count, func_ident, return_value)` - Function(u32, u32, Box), + Function(u32, u32, BumpBox<'a, JsValue<'a>>), // OPERATIONS // ---------------------------- /// A string concatenation of values. /// `foo.${unknownVar}.js` => 'foo' + Unknown + '.js' - Concat(u32, Vec), + Concat(u32, BumpVec<'a, JsValue<'a>>), /// An addition of values. /// This can be converted to [JsValue::Concat] if the type of the variable /// is string. - Add(u32, Vec), + Add(u32, BumpVec<'a, JsValue<'a>>), /// Logical negation `!expr` - Not(u32, Box), + Not(u32, BumpBox<'a, JsValue<'a>>), /// Logical operator chain e. g. `expr && expr` - Logical(u32, LogicalOperator, Vec), + Logical(u32, LogicalOperator, BumpVec<'a, JsValue<'a>>), /// Binary expression e. g. `expr == expr` - Binary(u32, Box, BinaryOperator, Box), + Binary( + u32, + BumpBox<'a, JsValue<'a>>, + BinaryOperator, + BumpBox<'a, JsValue<'a>>, + ), /// A constructor call. `(total_node_count, list)` — see [`CallList`]. - New(u32, CallList), + New(u32, CallList<'a>), /// A function call without a `this` context. `(total_node_count, list)` — see [`CallList`]. - Call(u32, CallList), + Call(u32, CallList<'a>), /// A super call to the parent constructor. /// `(total_node_count, args)` - SuperCall(u32, Vec), + SuperCall(u32, BumpBox<'a, [JsValue<'a>]>), /// A function call with a `this` context. `(total_node_count, list)` — see [`MemberCallList`]. - MemberCall(u32, MemberCallList), + MemberCall(u32, MemberCallList<'a>), /// A member access `obj[prop]` /// `(total_node_count, obj, prop)` - Member(u32, Box, Box), + Member(u32, BumpBox<'a, JsValue<'a>>, BumpBox<'a, JsValue<'a>>), /// A tenary operator `test ? cons : alt` /// `(total_node_count, test, cons, alt)` - Tenary(u32, Box, Box, Box), + Tenary( + u32, + BumpBox<'a, JsValue<'a>>, + BumpBox<'a, JsValue<'a>>, + BumpBox<'a, JsValue<'a>>, + ), /// A promise resolving to some value /// `(total_node_count, value)` - Promise(u32, Box), + Promise(u32, BumpBox<'a, JsValue<'a>>), /// An await call (potentially) unwrapping a promise. /// `(total_node_count, value)` - Awaited(u32, Box), + Awaited(u32, BumpBox<'a, JsValue<'a>>), /// A for-of loop /// /// `(total_node_count, iterable)` - Iterated(u32, Box), + Iterated(u32, BumpBox<'a, JsValue<'a>>), /// A `typeof` expression. /// /// `(total_node_count, operand)` - TypeOf(u32, Box), + TypeOf(u32, BumpBox<'a, JsValue<'a>>), // PLACEHOLDERS // ---------------------------- @@ -213,10 +224,10 @@ pub enum JsValue { /// (`MemberCall(total, obj, prop, [args])`) by writing obj/prop/args as siblings inside the /// parent's `debug_tuple`. This keeps fixture snapshots identical to the 4-tuple-payload /// version without forcing a hand-written `Debug` on every `JsValue` arm. -#[derive(Default, Clone, Hash, PartialEq)] -pub struct MemberCallList(Vec); +#[derive(Hash, PartialEq)] +pub struct MemberCallList<'a>(BumpVec<'a, JsValue<'a>>); -impl fmt::Debug for MemberCallList { +impl fmt::Debug for MemberCallList<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Layout: [args..., prop, obj] let n = self.0.len(); @@ -237,62 +248,73 @@ impl fmt::Debug for MemberCallList { } } -impl MemberCallList { - fn from_parts(obj: JsValue, prop: JsValue, args: Vec) -> Self { +impl<'a> MemberCallList<'a> { + fn from_parts( + arena: &'a Bump, + obj: JsValue<'a>, + prop: JsValue<'a>, + args: BumpVec<'a, JsValue<'a>>, + ) -> Self { let mut list = args; - list.reserve_exact(2); - list.push(prop); - list.push(obj); + list.push(arena, prop); + list.push(arena, obj); Self(list) } - fn from_iter(obj: JsValue, prop: JsValue, args: I) -> Self + fn from_iter(arena: &'a Bump, obj: JsValue<'a>, prop: JsValue<'a>, args: I) -> Self where - I: IntoIterator, + I: IntoIterator>, I::IntoIter: ExactSizeIterator, { let args = args.into_iter(); - let mut list = Vec::with_capacity(args.len() + 2); - list.extend(args); - list.push(prop); - list.push(obj); + let mut list = BumpVec::with_capacity_in(arena, args.len() + 2); + list.extend(arena, args); + list.push(arena, prop); + list.push(arena, obj); Self(list) } + fn clone_in(&self, arena: &'a Bump) -> Self { + Self(BumpVec::from_iter_in( + arena, + self.0.iter().map(|v| v.clone_in(arena)), + )) + } + /// The receiver object. Lives at the tail of the underlying `Vec`. - pub fn obj(&self) -> &JsValue { + pub fn obj(&self) -> &JsValue<'a> { &self.0[self.0.len() - 1] } - pub fn obj_mut(&mut self) -> &mut JsValue { + pub fn obj_mut(&mut self) -> &mut JsValue<'a> { let n = self.0.len(); &mut self.0[n - 1] } /// The accessed property. Lives one slot before `obj`. - pub fn prop(&self) -> &JsValue { + pub fn prop(&self) -> &JsValue<'a> { &self.0[self.0.len() - 2] } - pub fn prop_mut(&mut self) -> &mut JsValue { + pub fn prop_mut(&mut self) -> &mut JsValue<'a> { let n = self.0.len(); &mut self.0[n - 2] } /// The call arguments — everything before `prop` and `obj`. - pub fn args(&self) -> &[JsValue] { + pub fn args(&self) -> &[JsValue<'a>] { let n = self.0.len(); &self.0[..n - 2] } - pub fn args_mut(&mut self) -> &mut [JsValue] { + pub fn args_mut(&mut self) -> &mut [JsValue<'a>] { let n = self.0.len(); &mut self.0[..n - 2] } /// Borrow `args`, `prop`, and `obj` simultaneously as mutable references. The single /// `Vec` storage means callers can't get these via separate accessor calls. - pub fn as_parts_mut(&mut self) -> (&mut [JsValue], &mut JsValue, &mut JsValue) { + pub fn as_parts_mut(&mut self) -> (&mut [JsValue<'a>], &mut JsValue<'a>, &mut JsValue<'a>) { let n = self.0.len(); let (args, tail) = self.0.split_at_mut(n - 2); let (prop_slot, obj_slot) = tail.split_at_mut(1); @@ -301,7 +323,7 @@ impl MemberCallList { /// Take everything out. The returned `args` `Vec` reuses the original allocation — no /// copy. That's the point of storing obj/prop at the tail. - pub fn into_parts(mut self) -> (JsValue, JsValue, Vec) { + pub fn into_parts(mut self) -> (JsValue<'a>, JsValue<'a>, BumpVec<'a, JsValue<'a>>) { let obj = self.0.pop().unwrap(); let prop = self.0.pop().unwrap(); (obj, prop, self.0) @@ -311,10 +333,13 @@ impl MemberCallList { total_nodes(&self.0) } - fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue)) { + fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue<'a>)) { self.0.iter().for_each(visitor) } - fn for_each_children_mut(&mut self, visitor: &mut impl FnMut(&mut JsValue) -> bool) -> bool { + fn for_each_children_mut( + &mut self, + visitor: &mut impl FnMut(&mut JsValue<'a>) -> bool, + ) -> bool { let mut modified = false; for child in self.0.iter_mut() { if visitor(child) { @@ -338,10 +363,10 @@ impl MemberCallList { /// /// The custom `Debug` impl re-emits the pre-refactor `(callee, [args])` shape so fixture /// snapshots remain identical to the 3-tuple-payload version. -#[derive(Default, Clone, Hash, PartialEq)] -pub struct CallList(Vec); +#[derive(Hash, PartialEq)] +pub struct CallList<'a>(BumpVec<'a, JsValue<'a>>); -impl fmt::Debug for CallList { +impl fmt::Debug for CallList<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Layout: [args..., callee] let n = self.0.len(); @@ -358,51 +383,57 @@ impl fmt::Debug for CallList { } } -impl CallList { - fn from_parts(callee: JsValue, args: Vec) -> Self { +impl<'a> CallList<'a> { + fn from_parts(arena: &'a Bump, callee: JsValue<'a>, args: BumpVec<'a, JsValue<'a>>) -> Self { let mut list = args; - list.reserve_exact(1); - list.push(callee); + list.push(arena, callee); Self(list) } - fn from_iter(callee: JsValue, args: I) -> Self + fn from_iter(arena: &'a Bump, callee: JsValue<'a>, args: I) -> Self where - I: IntoIterator, + I: IntoIterator>, I::IntoIter: ExactSizeIterator, { let args = args.into_iter(); - let mut list = Vec::with_capacity(args.len() + 1); - list.extend(args); - list.push(callee); + let mut list = BumpVec::with_capacity_in(arena, args.len() + 1); + list.extend(arena, args); + list.push(arena, callee); Self(list) } + fn clone_in(&self, arena: &'a Bump) -> Self { + Self(BumpVec::from_iter_in( + arena, + self.0.iter().map(|v| v.clone_in(arena)), + )) + } + /// The callee. Lives at the tail of the underlying `Vec`. - pub fn callee(&self) -> &JsValue { + pub fn callee(&self) -> &JsValue<'a> { self.0.last().expect("CallList must always have a callee") } - pub fn callee_mut(&mut self) -> &mut JsValue { + pub fn callee_mut(&mut self) -> &mut JsValue<'a> { self.0 .last_mut() .expect("CallList must always have a callee") } /// The call arguments — everything before the callee. - pub fn args(&self) -> &[JsValue] { + pub fn args(&self) -> &[JsValue<'a>] { let n = self.0.len(); &self.0[..n - 1] } - pub fn args_mut(&mut self) -> &mut [JsValue] { + pub fn args_mut(&mut self) -> &mut [JsValue<'a>] { let n = self.0.len(); &mut self.0[..n - 1] } /// Borrow `args` and `callee` simultaneously as mutable references. The single `Vec` /// storage means callers can't get these via separate accessor calls. - pub fn as_parts_mut(&mut self) -> (&mut [JsValue], &mut JsValue) { + pub fn as_parts_mut(&mut self) -> (&mut [JsValue<'a>], &mut JsValue<'a>) { let n = self.0.len(); let (args, callee_slot) = self.0.split_at_mut(n - 1); (args, &mut callee_slot[0]) @@ -410,7 +441,7 @@ impl CallList { /// Take everything out. The returned `args` `Vec` reuses the original allocation — no /// copy. That's the point of storing the callee at the tail. - pub fn into_parts(mut self) -> (JsValue, Vec) { + pub fn into_parts(mut self) -> (JsValue<'a>, BumpVec<'a, JsValue<'a>>) { let callee = self.0.pop().unwrap(); (callee, self.0) } @@ -419,10 +450,13 @@ impl CallList { total_nodes(&self.0) } - fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue)) { + fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue<'a>)) { self.0.iter().for_each(visitor) } - fn for_each_children_mut(&mut self, visitor: &mut impl FnMut(&mut JsValue) -> bool) -> bool { + fn for_each_children_mut( + &mut self, + visitor: &mut impl FnMut(&mut JsValue<'a>) -> bool, + ) -> bool { let mut modified = false; for child in self.0.iter_mut() { if visitor(child) { @@ -438,64 +472,67 @@ impl CallList { } } -impl From<&'_ str> for JsValue { +impl<'a> From<&'_ str> for JsValue<'a> { fn from(v: &str) -> Self { ConstantValue::Str(ConstantString::Atom(v.into())).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: Atom) -> Self { ConstantValue::Str(ConstantString::Atom(v)).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: BigInt) -> Self { Self::from(Box::new(v)) } } -impl From> for JsValue { +impl<'a> From> for JsValue<'a> { fn from(v: Box) -> Self { ConstantValue::BigInt(v).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: f64) -> Self { ConstantValue::Num(ConstantNumber(v)).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: RcStr) -> Self { ConstantValue::Str(v.into()).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: String) -> Self { RcStr::from(v).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: swc_core::ecma::ast::Str) -> Self { ConstantValue::Str(ConstantString::Atom(v.value.to_atom_lossy().into_owned())).into() } } -impl From for JsValue { +impl<'a> From for JsValue<'a> { fn from(v: ConstantValue) -> Self { JsValue::Constant(v) } } -impl TryFrom<&CompileTimeDefineValue> for JsValue { - type Error = anyhow::Error; - - fn try_from(value: &CompileTimeDefineValue) -> Result { +impl<'a> JsValue<'a> { + /// Build a [`JsValue`] from a [`CompileTimeDefineValue`], allocating any nested structure in + /// `arena`. (Cannot be a `TryFrom` impl because the conversion needs the arena.) + pub fn from_compile_time_define_value_in( + arena: &'a Bump, + value: &CompileTimeDefineValue, + ) -> Result { Ok(JsValue::Constant(match value { CompileTimeDefineValue::Undefined => ConstantValue::Undefined, CompileTimeDefineValue::Null => ConstantValue::Null, @@ -510,33 +547,39 @@ impl TryFrom<&CompileTimeDefineValue> for JsValue { ConstantValue::Regex(Box::new((pattern.as_str().into(), flags.as_str().into()))) } CompileTimeDefineValue::Array(a) => { + let mut items = BumpVec::with_capacity_in(arena, a.len()); + for i in a { + items.push(arena, JsValue::from_compile_time_define_value_in(arena, i)?); + } let mut js_value = JsValue::Array { total_nodes: a.len() as u32, - items: a.iter().map(|i| i.try_into()).collect::>>()?, + items, mutable: false, }; js_value.update_total_nodes(); return Ok(js_value); } CompileTimeDefineValue::Object(m) => { + let mut parts = BumpVec::with_capacity_in(arena, m.len()); + for (k, v) in m { + parts.push( + arena, + ObjectPart::KeyValue( + k.clone().into(), + JsValue::from_compile_time_define_value_in(arena, v)?, + ), + ); + } let mut js_value = JsValue::Object { total_nodes: m.len() as u32, - parts: m - .iter() - .map(|(k, v)| { - Ok::(ObjectPart::KeyValue( - k.clone().into(), - v.try_into()?, - )) - }) - .collect::>>()?, + parts, mutable: false, }; js_value.update_total_nodes(); return Ok(js_value); } CompileTimeDefineValue::Evaluate(s) => { - return EvalContext::eval_single_expr_lit(s); + return EvalContext::eval_single_expr_lit(arena, s); } })) } @@ -565,12 +608,12 @@ impl TryFrom<&ConstantValue> for CompileTimeDefineValue { } } -impl TryFrom<&FreeVarReference> for JsValue { - type Error = anyhow::Error; - - fn try_from(value: &FreeVarReference) -> Result { +impl<'a> JsValue<'a> { + /// Build a [`JsValue`] from a [`FreeVarReference`], allocating any nested structure in `arena`. + /// (Cannot be a `TryFrom` impl because the conversion needs the arena.) + pub fn from_free_var_reference_in(arena: &'a Bump, value: &FreeVarReference) -> Result { match value { - FreeVarReference::Value(v) => v.try_into(), + FreeVarReference::Value(v) => JsValue::from_compile_time_define_value_in(arena, v), FreeVarReference::Ident(_) => Ok(JsValue::unknown_empty( false, rcstr!("compile time injected ident"), @@ -585,7 +628,7 @@ impl TryFrom<&FreeVarReference> for JsValue { )), FreeVarReference::ReportUsage { inner, .. } => { if let Some(inner) = &inner { - inner.as_ref().try_into() + JsValue::from_free_var_reference_in(arena, inner.as_ref()) } else { Ok(JsValue::unknown_empty( false, @@ -611,14 +654,14 @@ impl TryFrom<&FreeVarReference> for JsValue { } } -impl Default for JsValue { +impl Default for JsValue<'_> { fn default() -> Self { JsValue::unknown_empty(false, rcstr!("")) } } // Private meta methods -impl JsValue { +impl JsValue<'_> { fn meta_type(&self) -> JsValueMetaKind { match self { JsValue::Constant(..) @@ -654,8 +697,8 @@ impl JsValue { } // Constructors -impl JsValue { - pub fn alternatives(list: Vec) -> Self { +impl<'a> JsValue<'a> { + pub fn alternatives(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Alternatives { total_nodes: 1 + total_nodes(&list), values: list, @@ -664,7 +707,7 @@ impl JsValue { } pub fn alternatives_with_additional_property( - list: Vec, + list: BumpVec<'a, JsValue<'a>>, logical_property: LogicalProperty, ) -> Self { Self::Alternatives { @@ -674,23 +717,23 @@ impl JsValue { } } - pub fn concat(list: Vec) -> Self { + pub fn concat(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Concat(1 + total_nodes(&list), list) } - pub fn add(list: Vec) -> Self { + pub fn add(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Add(1 + total_nodes(&list), list) } - pub fn logical_and(list: Vec) -> Self { + pub fn logical_and(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Logical(1 + total_nodes(&list), LogicalOperator::And, list) } - pub fn logical_or(list: Vec) -> Self { + pub fn logical_or(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Logical(1 + total_nodes(&list), LogicalOperator::Or, list) } - pub fn nullish_coalescing(list: Vec) -> Self { + pub fn nullish_coalescing(list: BumpVec<'a, JsValue<'a>>) -> Self { Self::Logical( 1 + total_nodes(&list), LogicalOperator::NullishCoalescing, @@ -698,64 +741,64 @@ impl JsValue { ) } - pub fn tenary(test: Box, cons: Box, alt: Box) -> Self { + pub fn tenary(arena: &'a Bump, test: JsValue<'a>, cons: JsValue<'a>, alt: JsValue<'a>) -> Self { Self::Tenary( 1 + test.total_nodes() + cons.total_nodes() + alt.total_nodes(), - test, - cons, - alt, + BumpBox::new_in(test, arena), + BumpBox::new_in(cons, arena), + BumpBox::new_in(alt, arena), ) } - pub fn iterated(iterable: Box) -> Self { - Self::Iterated(1 + iterable.total_nodes(), iterable) + pub fn iterated(arena: &'a Bump, iterable: JsValue<'a>) -> Self { + Self::Iterated(1 + iterable.total_nodes(), BumpBox::new_in(iterable, arena)) } - pub fn equal(a: Box, b: Box) -> Self { + pub fn equal(arena: &'a Bump, a: JsValue<'a>, b: JsValue<'a>) -> Self { Self::Binary( 1 + a.total_nodes() + b.total_nodes(), - a, + BumpBox::new_in(a, arena), BinaryOperator::Equal, - b, + BumpBox::new_in(b, arena), ) } - pub fn not_equal(a: Box, b: Box) -> Self { + pub fn not_equal(arena: &'a Bump, a: JsValue<'a>, b: JsValue<'a>) -> Self { Self::Binary( 1 + a.total_nodes() + b.total_nodes(), - a, + BumpBox::new_in(a, arena), BinaryOperator::NotEqual, - b, + BumpBox::new_in(b, arena), ) } - pub fn strict_equal(a: Box, b: Box) -> Self { + pub fn strict_equal(arena: &'a Bump, a: JsValue<'a>, b: JsValue<'a>) -> Self { Self::Binary( 1 + a.total_nodes() + b.total_nodes(), - a, + BumpBox::new_in(a, arena), BinaryOperator::StrictEqual, - b, + BumpBox::new_in(b, arena), ) } - pub fn strict_not_equal(a: Box, b: Box) -> Self { + pub fn strict_not_equal(arena: &'a Bump, a: JsValue<'a>, b: JsValue<'a>) -> Self { Self::Binary( 1 + a.total_nodes() + b.total_nodes(), - a, + BumpBox::new_in(a, arena), BinaryOperator::StrictNotEqual, - b, + BumpBox::new_in(b, arena), ) } - pub fn logical_not(inner: Box) -> Self { - Self::Not(1 + inner.total_nodes(), inner) + pub fn logical_not(arena: &'a Bump, inner: JsValue<'a>) -> Self { + Self::Not(1 + inner.total_nodes(), BumpBox::new_in(inner, arena)) } - pub fn type_of(operand: Box) -> Self { - Self::TypeOf(1 + operand.total_nodes(), operand) + pub fn type_of(arena: &'a Bump, operand: JsValue<'a>) -> Self { + Self::TypeOf(1 + operand.total_nodes(), BumpBox::new_in(operand, arena)) } - pub fn array(items: Vec) -> Self { + pub fn array(items: BumpVec<'a, JsValue<'a>>) -> Self { Self::Array { total_nodes: 1 + total_nodes(&items), items, @@ -763,7 +806,7 @@ impl JsValue { } } - pub fn frozen_array(items: Vec) -> Self { + pub fn frozen_array(items: BumpVec<'a, JsValue<'a>>) -> Self { Self::Array { total_nodes: 1 + total_nodes(&items), items, @@ -772,27 +815,28 @@ impl JsValue { } pub fn function( + arena: &'a Bump, func_ident: u32, is_async: bool, is_generator: bool, - return_value: JsValue, + return_value: JsValue<'a>, ) -> Self { // Check generator first to handle async generators let return_value = if is_generator { JsValue::WellKnownObject(WellKnownObjectKind::Generator) } else if is_async { - JsValue::promise(return_value) + JsValue::promise(arena, return_value) } else { return_value }; Self::Function( 1 + return_value.total_nodes(), func_ident, - Box::new(return_value), + BumpBox::new_in(return_value, arena), ) } - pub fn object(list: Vec) -> Self { + pub fn object(list: BumpVec<'a, ObjectPart<'a>>) -> Self { Self::Object { total_nodes: 1 + list .iter() @@ -806,7 +850,7 @@ impl JsValue { } } - pub fn frozen_object(list: Vec) -> Self { + pub fn frozen_object(list: BumpVec<'a, ObjectPart<'a>>) -> Self { Self::Object { total_nodes: 1 + list .iter() @@ -828,21 +872,21 @@ impl JsValue { /// slot (e.g. an `args` Vec returned from [`CallList::into_parts`] or /// [`MemberCallList::into_parts`]). For from-scratch construction use /// [`JsValue::new_from_iter`], which pre-sizes the underlying allocation exactly. - pub fn new_from_parts(f: JsValue, args: Vec) -> Self { + pub fn new_from_parts(arena: &'a Bump, f: JsValue<'a>, args: BumpVec<'a, JsValue<'a>>) -> Self { let total = 1 + f.total_nodes() + total_nodes(&args); - Self::New(total, CallList::from_parts(f, args)) + Self::New(total, CallList::from_parts(arena, f, args)) } /// Build a `JsValue::New` from a callee and an args iterator with a known length. /// /// Allocates the underlying `Vec` with exact capacity (`args.len() + 1`), so no realloc /// occurs. - pub fn new_from_iter(f: JsValue, args: I) -> Self + pub fn new_from_iter(arena: &'a Bump, f: JsValue<'a>, args: I) -> Self where - I: IntoIterator, + I: IntoIterator>, I::IntoIter: ExactSizeIterator, { - let list = CallList::from_iter(f, args); + let list = CallList::from_iter(arena, f, args); let total = 1 + total_nodes(&list.0); Self::New(total, list) } @@ -853,26 +897,30 @@ impl JsValue { /// caller already has a `Vec` that is likely to be correctly sized (typically one /// obtained from [`CallList::into_parts`] / [`MemberCallList::into_parts`]). For /// from-scratch construction use [`JsValue::call_from_iter`]. - pub fn call_from_parts(f: JsValue, args: Vec) -> Self { + pub fn call_from_parts( + arena: &'a Bump, + f: JsValue<'a>, + args: BumpVec<'a, JsValue<'a>>, + ) -> Self { let total = 1 + f.total_nodes() + total_nodes(&args); - Self::Call(total, CallList::from_parts(f, args)) + Self::Call(total, CallList::from_parts(arena, f, args)) } /// Build a `JsValue::Call` from a callee and an args iterator with a known length. /// /// Allocates the underlying `Vec` with exact capacity (`args.len() + 1`), so no realloc /// occurs. - pub fn call_from_iter(f: JsValue, args: I) -> Self + pub fn call_from_iter(arena: &'a Bump, f: JsValue<'a>, args: I) -> Self where - I: IntoIterator, + I: IntoIterator>, I::IntoIter: ExactSizeIterator, { - let list = CallList::from_iter(f, args); + let list = CallList::from_iter(arena, f, args); let total = 1 + total_nodes(&list.0); Self::Call(total, list) } - pub fn super_call(args: Vec) -> Self { + pub fn super_call(args: BumpBox<'a, [JsValue<'a>]>) -> Self { Self::SuperCall(1 + total_nodes(&args), args) } @@ -882,9 +930,14 @@ impl JsValue { /// caller already has a `Vec` that is likely to be correctly sized (typically one /// obtained from [`MemberCallList::into_parts`]). For from-scratch construction use /// [`JsValue::member_call_from_iter`]. - pub fn member_call_from_parts(o: JsValue, p: JsValue, args: Vec) -> Self { + pub fn member_call_from_parts( + arena: &'a Bump, + o: JsValue<'a>, + p: JsValue<'a>, + args: BumpVec<'a, JsValue<'a>>, + ) -> Self { let total = 1 + o.total_nodes() + p.total_nodes() + total_nodes(&args); - Self::MemberCall(total, MemberCallList::from_parts(o, p, args)) + Self::MemberCall(total, MemberCallList::from_parts(arena, o, p, args)) } /// Build a `JsValue::MemberCall` from `obj`, `prop`, and an args iterator with a known @@ -892,33 +945,42 @@ impl JsValue { /// /// Allocates the underlying `Vec` with exact capacity (`args.len() + 2`), so no realloc /// occurs. - pub fn member_call_from_iter(o: JsValue, p: JsValue, args: I) -> Self + pub fn member_call_from_iter( + arena: &'a Bump, + o: JsValue<'a>, + p: JsValue<'a>, + args: I, + ) -> Self where - I: IntoIterator, + I: IntoIterator>, I::IntoIter: ExactSizeIterator, { - let list = MemberCallList::from_iter(o, p, args); + let list = MemberCallList::from_iter(arena, o, p, args); let total = 1 + total_nodes(&list.0); Self::MemberCall(total, list) } - pub fn member(o: Box, p: Box) -> Self { - Self::Member(1 + o.total_nodes() + p.total_nodes(), o, p) + pub fn member(arena: &'a Bump, o: JsValue<'a>, p: JsValue<'a>) -> Self { + Self::Member( + 1 + o.total_nodes() + p.total_nodes(), + BumpBox::new_in(o, arena), + BumpBox::new_in(p, arena), + ) } - pub fn promise(operand: JsValue) -> Self { + pub fn promise(arena: &'a Bump, operand: JsValue<'a>) -> Self { // In ecmascript Promise> is equivalent to Promise if let JsValue::Promise(_, _) = operand { return operand; } - Self::Promise(1 + operand.total_nodes(), Box::new(operand)) + Self::Promise(1 + operand.total_nodes(), BumpBox::new_in(operand, arena)) } - pub fn awaited(operand: Box) -> Self { - Self::Awaited(1 + operand.total_nodes(), operand) + pub fn awaited(arena: &'a Bump, operand: JsValue<'a>) -> Self { + Self::Awaited(1 + operand.total_nodes(), BumpBox::new_in(operand, arena)) } - pub fn unknown(value: impl Into>, side_effects: bool, reason: RcStr) -> Self { + pub fn unknown(value: impl Into>>, side_effects: bool, reason: RcStr) -> Self { Self::Unknown { original_value: Some(value.into()), reason, @@ -934,7 +996,12 @@ impl JsValue { } } - pub fn unknown_if(is_unknown: bool, value: JsValue, side_effects: bool, reason: RcStr) -> Self { + pub fn unknown_if( + is_unknown: bool, + value: JsValue<'a>, + side_effects: bool, + reason: RcStr, + ) -> Self { if is_unknown { Self::Unknown { original_value: Some(value.into()), @@ -948,7 +1015,7 @@ impl JsValue { } // Methods regarding node count -impl JsValue { +impl JsValue<'_> { pub fn has_children(&self) -> bool { self.total_nodes() > 1 } @@ -1089,7 +1156,7 @@ impl JsValue { } // Unknown management -impl JsValue { +impl<'a> JsValue<'a> { /// Convert the value into unknown with a specific reason. pub fn make_unknown(&mut self, side_effects: bool, reason: RcStr) { *self = JsValue::unknown(take(self), side_effects || self.has_side_effects(), reason); @@ -1124,16 +1191,16 @@ impl JsValue { } } - pub fn add_unknown_mutations(&mut self, side_effects: bool) { - self.add_alt(JsValue::unknown_empty( - side_effects, - rcstr!("unknown mutation"), - )); + pub fn add_unknown_mutations(&mut self, arena: &'a Bump, side_effects: bool) { + self.add_alt( + arena, + JsValue::unknown_empty(side_effects, rcstr!("unknown mutation")), + ); } } // Definable name management -impl JsValue { +impl JsValue<'_> { /// When the value has a user-definable name, return it in segments. Otherwise /// returns None. /// It also returns a boolean whether the variable was potentially reassigned. @@ -1144,7 +1211,7 @@ impl JsValue { /// - typeof expressions add `typeof` after the argument's segments: ["foo", "typeof"] pub fn get_definable_name( &self, - var_graph: Option<&VarGraph>, + var_graph: Option<&VarGraph<'_>>, ) -> Option<(DefinableNameSegmentRefs<'_>, bool)> { let mut current = self; let mut segments = SmallVec::new(); @@ -1204,6 +1271,115 @@ impl JsValue { } } +// Arena-aware cloning (replaces the derived `Clone`, which is unavailable because the arena-backed +// `Box`/`Vec` children can't clone without the allocator). +impl<'a> JsValue<'a> { + /// Deep-clone this value into `arena`, returning a fresh tree owned by that arena. + pub fn clone_in(&self, arena: &'a Bump) -> JsValue<'a> { + match self { + JsValue::Constant(v) => JsValue::Constant(v.clone()), + JsValue::Url(s, k) => JsValue::Url(s.clone(), *k), + JsValue::WellKnownObject(k) => JsValue::WellKnownObject(k.clone()), + JsValue::WellKnownFunction(k) => JsValue::WellKnownFunction(k.clone()), + JsValue::Unknown { + original_value, + reason, + has_side_effects, + } => JsValue::Unknown { + original_value: original_value.clone(), + reason: reason.clone(), + has_side_effects: *has_side_effects, + }, + JsValue::Array { + total_nodes, + items, + mutable, + } => JsValue::Array { + total_nodes: *total_nodes, + items: BumpVec::from_iter_in(arena, items.iter().map(|v| v.clone_in(arena))), + mutable: *mutable, + }, + JsValue::Object { + total_nodes, + parts, + mutable, + } => JsValue::Object { + total_nodes: *total_nodes, + parts: BumpVec::from_iter_in(arena, parts.iter().map(|p| p.clone_in(arena))), + mutable: *mutable, + }, + JsValue::Alternatives { + total_nodes, + values, + logical_property, + } => JsValue::Alternatives { + total_nodes: *total_nodes, + values: BumpVec::from_iter_in(arena, values.iter().map(|v| v.clone_in(arena))), + logical_property: *logical_property, + }, + JsValue::Function(c, id, r) => { + JsValue::Function(*c, *id, BumpBox::new_in(r.clone_in(arena), arena)) + } + JsValue::Concat(c, list) => JsValue::Concat( + *c, + BumpVec::from_iter_in(arena, list.iter().map(|v| v.clone_in(arena))), + ), + JsValue::Add(c, list) => JsValue::Add( + *c, + BumpVec::from_iter_in(arena, list.iter().map(|v| v.clone_in(arena))), + ), + JsValue::Not(c, v) => JsValue::Not(*c, BumpBox::new_in(v.clone_in(arena), arena)), + JsValue::Logical(c, op, list) => JsValue::Logical( + *c, + *op, + BumpVec::from_iter_in(arena, list.iter().map(|v| v.clone_in(arena))), + ), + JsValue::Binary(c, a, op, b) => JsValue::Binary( + *c, + BumpBox::new_in(a.clone_in(arena), arena), + *op, + BumpBox::new_in(b.clone_in(arena), arena), + ), + JsValue::New(c, call) => JsValue::New(*c, call.clone_in(arena)), + JsValue::Call(c, call) => JsValue::Call(*c, call.clone_in(arena)), + JsValue::SuperCall(c, args) => JsValue::SuperCall( + *c, + bumpalo::collections::Vec::from_iter_in( + args.iter().map(|v| v.clone_in(arena)), + arena, + ) + .into_boxed_slice(), + ), + JsValue::MemberCall(c, call) => JsValue::MemberCall(*c, call.clone_in(arena)), + JsValue::Member(c, o, p) => JsValue::Member( + *c, + BumpBox::new_in(o.clone_in(arena), arena), + BumpBox::new_in(p.clone_in(arena), arena), + ), + JsValue::Tenary(c, test, cons, alt) => JsValue::Tenary( + *c, + BumpBox::new_in(test.clone_in(arena), arena), + BumpBox::new_in(cons.clone_in(arena), arena), + BumpBox::new_in(alt.clone_in(arena), arena), + ), + JsValue::Promise(c, v) => { + JsValue::Promise(*c, BumpBox::new_in(v.clone_in(arena), arena)) + } + JsValue::Awaited(c, v) => { + JsValue::Awaited(*c, BumpBox::new_in(v.clone_in(arena), arena)) + } + JsValue::Iterated(c, v) => { + JsValue::Iterated(*c, BumpBox::new_in(v.clone_in(arena), arena)) + } + JsValue::TypeOf(c, v) => JsValue::TypeOf(*c, BumpBox::new_in(v.clone_in(arena), arena)), + JsValue::Variable(id) => JsValue::Variable(id.clone()), + JsValue::Argument(i, idx) => JsValue::Argument(*i, *idx), + JsValue::FreeVar(a) => JsValue::FreeVar(a.clone()), + JsValue::Module(m) => JsValue::Module(m.clone()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/normalize.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/normalize.rs index 0f7e60562227..438a2d673310 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/normalize.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/normalize.rs @@ -3,14 +3,14 @@ use std::{hash::BuildHasherDefault, mem::take}; use rustc_hash::FxHasher; use turbo_tasks::FxIndexSet; -use crate::analyzer::{JsValue, jsvalue::similar::SimilarJsValue}; +use crate::analyzer::{Bump, BumpVec, JsValue, jsvalue::similar::SimilarJsValue}; // Alternatives management -impl JsValue { +impl<'a> JsValue<'a> { /// Add an alternative to the current value. Might be a no-op if the value /// already contains this alternative. Potentially expensive operation /// as it has to compare the value with all existing alternatives. - pub(crate) fn add_alt(&mut self, v: Self) { + pub(crate) fn add_alt(&mut self, arena: &'a Bump, v: Self) { if self == &v { return; } @@ -23,13 +23,13 @@ impl JsValue { { if !values.contains(&v) { *c += v.total_nodes(); - values.push(v); + values.push(arena, v); } } else { let l = take(self); *self = JsValue::Alternatives { total_nodes: 1 + l.total_nodes() + v.total_nodes(), - values: vec![l, v], + values: BumpVec::from_iter_in(arena, [l, v]), logical_property: None, }; } @@ -37,10 +37,10 @@ impl JsValue { } // Normalization -impl JsValue { +impl<'a> JsValue<'a> { /// Normalizes only the current node. Nested alternatives, concatenations, /// or operations are collapsed. - pub fn normalize_shallow(&mut self) { + pub fn normalize_shallow(&mut self, arena: &'a Bump) { match self { JsValue::Alternatives { total_nodes: _, @@ -54,7 +54,9 @@ impl JsValue { values.len(), BuildHasherDefault::::default(), ); - for v in take(values) { + // Take the children out so we can rebuild `values` in place. + let taken = take(values); + for v in taken { match v { JsValue::Alternatives { total_nodes: _, @@ -73,32 +75,34 @@ impl JsValue { if set.len() == 1 { *self = set.into_iter().next().unwrap().0; } else { - *values = set.into_iter().map(|v| v.0).collect(); + *values = BumpVec::from_iter_in(arena, set.into_iter().map(|v| v.0)); self.update_total_nodes(); } } } JsValue::Concat(_, v) => { - // Remove empty strings - v.retain(|v| v.as_str() != Some("")); - // TODO(kdy1): Remove duplicate - let mut new: Vec = vec![]; - for v in take(v) { + let taken = take(v); + let mut new: BumpVec = BumpVec::with_capacity_in(arena, taken.len()); + for v in taken { + // Remove empty strings + if v.as_str() == Some("") { + continue; + } if let Some(str) = v.as_str() { if let Some(last) = new.last_mut() { if let Some(last_str) = last.as_str() { *last = [last_str, str].concat().into(); } else { - new.push(v); + new.push(arena, v); } } else { - new.push(v); + new.push(arena, v); } } else if let JsValue::Concat(_, v) = v { - new.extend(v); + new.extend(arena, v); } else { - new.push(v); + new.push(arena, v); } } if new.len() == 1 { @@ -109,29 +113,31 @@ impl JsValue { } } JsValue::Add(_, v) => { - let mut added: Vec = Vec::new(); - let mut iter = take(v).into_iter(); + let taken = take(v); + let mut added: BumpVec = BumpVec::with_capacity_in(arena, taken.len()); + let mut iter = taken.into_iter(); while let Some(item) = iter.next() { if item.is_string() == Some(true) { - let mut concat = match added.len() { - 0 => Vec::new(), - 1 => vec![added.into_iter().next().unwrap()], - _ => vec![JsValue::Add( - 1 + added.iter().map(|v| v.total_nodes()).sum::(), - added, - )], + let mut concat: BumpVec = match added.len() { + 0 => BumpVec::new(), + 1 => BumpVec::from_iter_in(arena, [added.into_iter().next().unwrap()]), + _ => BumpVec::from_iter_in( + arena, + [JsValue::Add( + 1 + added.iter().map(|v| v.total_nodes()).sum::(), + added, + )], + ), }; - concat.push(item); - for item in iter.by_ref() { - concat.push(item); - } + concat.push(arena, item); + concat.extend(arena, iter); *self = JsValue::Concat( 1 + concat.iter().map(|v| v.total_nodes()).sum::(), concat, ); return; } else { - added.push(item); + added.push(arena, item); } } if added.len() == 1 { @@ -152,15 +158,16 @@ impl JsValue { } }) => { // Taking the old list and constructing a new merged list - for mut v in take(list).into_iter() { + let taken = take(list); + for mut v in taken { if let JsValue::Logical(_, inner_op, inner_list) = &mut v { if inner_op == op { - list.append(inner_list); + list.extend(arena, take(inner_list)); } else { - list.push(v); + list.push(arena, v); } } else { - list.push(v); + list.push(arena, v); } } self.update_total_nodes(); @@ -170,12 +177,12 @@ impl JsValue { } /// Normalizes the current node and all nested nodes. - pub fn normalize(&mut self) { + pub fn normalize(&mut self, arena: &'a Bump) { self.for_each_children_mut(&mut |child| { - child.normalize(); + child.normalize(arena); true }); - self.normalize_shallow(); + self.normalize_shallow(arena); } } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/predicates.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/predicates.rs index 82d8b625ecf6..5db78623b515 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/predicates.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/predicates.rs @@ -4,7 +4,7 @@ use crate::analyzer::{ }; // Compile-time information gathering -impl JsValue { +impl JsValue<'_> { /// Returns the constant string if the value represents a constant string. pub fn as_str(&self) -> Option<&str> { match self { @@ -101,9 +101,9 @@ impl JsValue { shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_truthy) } }, - JsValue::Binary(_, box a, op, box b) => { + JsValue::Binary(_, a, op, b) => { let (positive_op, negate) = op.positive_op(); - match (positive_op, a, b) { + match (positive_op, &**a, &**b) { ( PositiveBinaryOperator::StrictEqual, JsValue::Constant(a), @@ -478,14 +478,21 @@ mod tests { use rstest::rstest; use turbo_rcstr::rcstr; - use crate::analyzer::{ConstantValue, JsValue, graph::EvalContext}; + use crate::analyzer::{Bump, ConstantValue, JsValue, ThreadLocal, graph::EvalContext}; + + // A leaked arena for building test `JsValue`s with a `'static` lifetime. Tests are + // short-lived processes, so the leak is inconsequential. + fn test_arena() -> &'static Bump { + Box::leak(Box::new(Bump::new())) + } // `construct_test_ternary(cons, alt)` builds a ternary with an unknown test condition. - fn construct_test_ternary(cons: JsValue, alt: JsValue) -> JsValue { + fn construct_test_ternary(cons: JsValue<'static>, alt: JsValue<'static>) -> JsValue<'static> { JsValue::tenary( - Box::new(JsValue::unknown_empty(false, rcstr!("test"))), - Box::new(cons), - Box::new(alt), + test_arena(), + JsValue::unknown_empty(false, rcstr!("test")), + cons, + alt, ) } @@ -493,9 +500,9 @@ mod tests { #[case(JsValue::from(1.0))] #[case(JsValue::from("hi"))] #[case(ConstantValue::True.into())] - #[case(JsValue::promise(ConstantValue::Null.into()))] + #[case(JsValue::promise(test_arena(), ConstantValue::Null.into()))] #[case(construct_test_ternary(JsValue::from(1.0), JsValue::from("hi")))] - fn is_truthy_positive(#[case] v: JsValue) { + fn is_truthy_positive(#[case] v: JsValue<'static>) { assert_eq!(v.is_truthy(), Some(true), "expected '{v}' to be truthy"); } @@ -506,7 +513,7 @@ mod tests { #[case(ConstantValue::Null.into())] #[case(ConstantValue::Undefined.into())] #[case(construct_test_ternary(JsValue::from(0.0), JsValue::from("")))] - fn is_truthy_negative(#[case] v: JsValue) { + fn is_truthy_negative(#[case] v: JsValue<'static>) { assert_eq!(v.is_truthy(), Some(false), "expected '{v}' to be falsy"); } @@ -514,7 +521,7 @@ mod tests { #[case(ConstantValue::Null.into())] #[case(ConstantValue::Undefined.into())] #[case(construct_test_ternary(ConstantValue::Null.into(), ConstantValue::Undefined.into()))] - fn is_nullish_positive(#[case] v: JsValue) { + fn is_nullish_positive(#[case] v: JsValue<'static>) { assert_eq!(v.is_nullish(), Some(true), "expected '{v}' to be nullish"); } @@ -523,9 +530,9 @@ mod tests { #[case(JsValue::from(""))] #[case(JsValue::from("hi"))] #[case(ConstantValue::True.into())] - #[case(JsValue::promise(ConstantValue::Null.into()))] + #[case(JsValue::promise(test_arena(), ConstantValue::Null.into()))] #[case(construct_test_ternary(JsValue::from(0.0), JsValue::from("hi")))] - fn is_nullish_negative(#[case] v: JsValue) { + fn is_nullish_negative(#[case] v: JsValue<'static>) { assert_eq!( v.is_nullish(), Some(false), @@ -537,7 +544,7 @@ mod tests { #[case(JsValue::from("hi"))] #[case(JsValue::from(""))] #[case(construct_test_ternary(JsValue::from("a"), JsValue::from("b")))] - fn is_string_positive(#[case] v: JsValue) { + fn is_string_positive(#[case] v: JsValue<'static>) { assert_eq!(v.is_string(), Some(true), "expected '{v}' to be a string"); } @@ -546,7 +553,7 @@ mod tests { #[case(ConstantValue::True.into())] #[case(ConstantValue::Null.into())] #[case(construct_test_ternary(JsValue::from(1.0), JsValue::from(2.0)))] - fn is_string_negative(#[case] v: JsValue) { + fn is_string_negative(#[case] v: JsValue<'static>) { assert_eq!( v.is_string(), Some(false), @@ -557,7 +564,7 @@ mod tests { #[rstest] #[case(JsValue::from(""))] #[case(construct_test_ternary(JsValue::from(""), JsValue::from("")))] - fn is_empty_string_positive(#[case] v: JsValue) { + fn is_empty_string_positive(#[case] v: JsValue<'static>) { assert_eq!( v.is_empty_string(), Some(true), @@ -570,7 +577,7 @@ mod tests { #[case(JsValue::from(1.0))] #[case(ConstantValue::True.into())] #[case(construct_test_ternary(JsValue::from("a"), JsValue::from("b")))] - fn is_empty_string_negative(#[case] v: JsValue) { + fn is_empty_string_negative(#[case] v: JsValue<'static>) { assert_eq!( v.is_empty_string(), Some(false), @@ -580,7 +587,9 @@ mod tests { #[test] fn is_string_constant() { - let value = EvalContext::eval_single_expr_lit(&rcstr!("'hello'")).unwrap(); + let arena = ThreadLocal::new(); + let value = + EvalContext::eval_single_expr_lit(arena.get_or_default(), &rcstr!("'hello'")).unwrap(); assert_eq!(value.is_string(), Some(true)); } @@ -588,8 +597,9 @@ mod tests { #[case("1 && 'hello'")] #[case("'hello' || 'bye' || 2")] fn is_string_short_circuiting_positive(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_string(), Some(true), @@ -602,8 +612,9 @@ mod tests { #[case("'hello' && 2")] #[case("2 || 1 || 'hello' || 'bye'")] fn is_string_short_circuiting_negative(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_string(), Some(false), @@ -619,8 +630,9 @@ mod tests { #[case("x || 'bye'")] #[case("false || x")] fn is_string_short_circuiting_unknown(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_string(), None, @@ -634,8 +646,9 @@ mod tests { #[case("false || ''")] #[case("1 && 'a' && ''")] fn is_empty_string_short_circuiting_positive(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_empty_string(), Some(true), @@ -649,8 +662,9 @@ mod tests { #[case("'' || 'string'")] #[case("'' || 0 || 'string'")] fn is_empty_string_short_circuiting_negative(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_empty_string(), Some(false), @@ -666,8 +680,9 @@ mod tests { #[case("'' || x")] #[case("false || 0 || x")] fn is_empty_string_short_circuiting_unknown(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_empty_string(), None, @@ -681,8 +696,9 @@ mod tests { #[case("'' || null")] #[case("1 && 2 && null")] fn is_nullish_short_circuiting_positive(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_nullish(), Some(true), @@ -696,8 +712,9 @@ mod tests { #[case("null || ''")] #[case("null || '' || 'a'")] fn is_nullish_short_circuiting_negative(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_nullish(), Some(false), @@ -714,8 +731,9 @@ mod tests { #[case("false || x")] #[case("1 && x && null")] fn is_nullish_short_circuiting_unknown(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_nullish(), None, @@ -729,8 +747,9 @@ mod tests { #[case("null || ''")] #[case("null || 0 || 'a'")] fn is_not_nullish_short_circuiting_positive(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_not_nullish(), Some(true), @@ -744,8 +763,9 @@ mod tests { #[case("'' || null")] #[case("'' || 0 || null")] fn is_not_nullish_short_circuiting_negative(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_not_nullish(), Some(false), @@ -762,8 +782,9 @@ mod tests { #[case("false || x")] #[case("false || x || ''")] fn is_not_nullish_short_circuiting_unknown(#[case] input: &str) { + let arena = ThreadLocal::new(); assert_eq!( - EvalContext::eval_single_expr_lit(&input.into()) + EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into()) .unwrap() .is_not_nullish(), None, diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/similar.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/similar.rs index 6af69f5a892e..e32d272dcd2e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/similar.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/similar.rs @@ -3,8 +3,8 @@ use std::hash::Hash; use crate::analyzer::{CallList, JsValue, MemberCallList, ModuleValue, ObjectPart}; // Like equality, but with depth limit -impl JsValue { - pub(super) fn all_similar(a: &[JsValue], b: &[JsValue], depth: usize) -> bool { +impl<'a> JsValue<'a> { + pub(super) fn all_similar(a: &[JsValue<'a>], b: &[JsValue<'a>], depth: usize) -> bool { if a.len() != b.len() { return false; } @@ -12,12 +12,12 @@ impl JsValue { } /// Check if the values are equal up to the given depth. Might return false /// even if the values are equal when hitting the depth limit. - fn similar(&self, other: &JsValue, depth: usize) -> bool { + fn similar(&self, other: &JsValue<'a>, depth: usize) -> bool { if depth == 0 { return false; } - fn all_parts_similar(a: &[ObjectPart], b: &[ObjectPart], depth: usize) -> bool { + fn all_parts_similar<'b>(a: &[ObjectPart<'b>], b: &[ObjectPart<'b>], depth: usize) -> bool { if a.len() != b.len() { return false; } @@ -134,14 +134,18 @@ impl JsValue { return; } - fn all_similar_hash(slice: &[JsValue], state: &mut H, depth: usize) { + fn all_similar_hash( + slice: &[JsValue<'_>], + state: &mut H, + depth: usize, + ) { for item in slice { item.similar_hash(state, depth); } } fn all_parts_similar_hash( - slice: &[ObjectPart], + slice: &[ObjectPart<'_>], state: &mut H, depth: usize, ) { @@ -178,12 +182,12 @@ impl JsValue { | JsValue::Logical(_, _, v) => all_similar_hash(v, state, depth - 1), JsValue::Not(_, v) => v.similar_hash(state, depth - 1), JsValue::New(_, call) => { - call.for_each_children(&mut |child: &JsValue| { + call.for_each_children(&mut |child: &JsValue<'_>| { child.similar_hash(state, depth - 1); }); } JsValue::Call(_, call) => { - call.for_each_children(&mut |child: &JsValue| { + call.for_each_children(&mut |child: &JsValue<'_>| { child.similar_hash(state, depth - 1); }); } @@ -191,7 +195,7 @@ impl JsValue { all_similar_hash(args, state, depth - 1); } JsValue::MemberCall(_, call) => { - call.for_each_children(&mut |child: &JsValue| { + call.for_each_children(&mut |child: &JsValue<'_>| { child.similar_hash(state, depth - 1); }); } @@ -249,17 +253,17 @@ const SIMILAR_HASH_DEPTH: usize = 2; /// A wrapper around `JsValue` that implements `PartialEq` and `Hash` by /// comparing the values with a depth of [SIMILAR_EQ_DEPTH] and hashing values /// with a depth of [SIMILAR_HASH_DEPTH]. -pub(super) struct SimilarJsValue(pub(super) JsValue); +pub(super) struct SimilarJsValue<'a>(pub(super) JsValue<'a>); -impl PartialEq for SimilarJsValue { +impl PartialEq for SimilarJsValue<'_> { fn eq(&self, other: &Self) -> bool { self.0.similar(&other.0, SIMILAR_EQ_DEPTH) } } -impl Eq for SimilarJsValue {} +impl Eq for SimilarJsValue<'_> {} -impl Hash for SimilarJsValue { +impl Hash for SimilarJsValue<'_> { fn hash(&self, state: &mut H) { self.0.similar_hash(state, SIMILAR_HASH_DEPTH) } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/traverse.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/traverse.rs index 951edc6bbf94..87b81dcaeb5a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/traverse.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/jsvalue/traverse.rs @@ -1,12 +1,12 @@ use crate::analyzer::{JsValue, ObjectPart}; // Visiting -impl JsValue { +impl<'a> JsValue<'a> { /// Calls a function for each child of the node. Allows mutating the node. /// Updates the total nodes count after mutation. pub fn for_each_children_mut( &mut self, - visitor: &mut impl FnMut(&mut JsValue) -> bool, + visitor: &mut impl FnMut(&mut JsValue<'a>) -> bool, ) -> bool { match self { JsValue::Alternatives { @@ -158,7 +158,7 @@ impl JsValue { /// node. Updates the total nodes count after mutation. pub fn for_each_early_children_mut( &mut self, - visitor: &mut impl FnMut(&mut JsValue) -> bool, + visitor: &mut impl FnMut(&mut JsValue<'a>) -> bool, ) -> bool { match self { JsValue::New(_, call) if !call.args().is_empty() => { @@ -199,7 +199,7 @@ impl JsValue { /// node. Updates the total nodes count after mutation. pub fn for_each_late_children_mut( &mut self, - visitor: &mut impl FnMut(&mut JsValue) -> bool, + visitor: &mut impl FnMut(&mut JsValue<'a>) -> bool, ) -> bool { match self { JsValue::New(_, call) if !call.args().is_empty() => { @@ -250,13 +250,13 @@ impl JsValue { } /// Visit the node and all its children with a function. - pub fn visit(&self, visitor: &mut impl FnMut(&JsValue)) { + pub fn visit(&self, visitor: &mut impl FnMut(&JsValue<'a>)) { self.for_each_children(&mut |value| value.visit(visitor)); visitor(self); } /// Calls a function for all children of the node. - pub fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue)) { + pub fn for_each_children(&self, visitor: &mut impl FnMut(&JsValue<'a>)) { match self { JsValue::Alternatives { total_nodes: _, diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/linker.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/linker.rs index a0e15768bb0a..b5efae0ebe3d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/linker.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/linker.rs @@ -6,24 +6,27 @@ use rustc_hash::{FxHashMap, FxHashSet}; use swc_core::ecma::ast::Id; use turbo_rcstr::rcstr; -use super::{JsValue, graph::VarGraph}; +use super::{Bump, JsValue, ThreadLocal, graph::VarGraph}; +use crate::analyzer::BumpVec; -pub async fn link<'a, B, RB, F, RF>( - graph: &VarGraph, - mut val: JsValue, - early_visitor: &B, - visitor: &F, - fun_args_values: &Mutex>>, - var_cache: &Mutex>, -) -> Result<(JsValue, u32)> +pub async fn link<'a, 'l, B, RB, F, RF>( + arena: &'a ThreadLocal, + graph: &'l VarGraph<'a>, + mut val: JsValue<'a>, + early_visitor: &'l B, + visitor: &'l F, + fun_args_values: &Mutex>>>, + var_cache: &Mutex>>, +) -> Result<(JsValue<'a>, u32)> where - RB: 'a + Future> + Send, - B: 'a + Fn(JsValue) -> RB + Sync, - RF: 'a + Future> + Send, - F: 'a + Fn(JsValue) -> RF + Sync, + RB: 'l + Future, bool)>> + Send, + B: 'l + Fn(JsValue<'a>) -> RB + Sync, + RF: 'l + Future, bool)>> + Send, + F: 'l + Fn(JsValue<'a>) -> RF + Sync, { - val.normalize(); + val.normalize(arena.get_or_default()); let (val, steps) = link_internal_iterative( + arena, graph, val, early_visitor, @@ -39,19 +42,19 @@ const LIMIT_NODE_SIZE: u32 = 100; const LIMIT_IN_PROGRESS_NODES: u32 = 500; const LIMIT_LINK_STEPS: u32 = 1500; -#[derive(Debug, Clone, PartialEq)] -enum Step { +#[derive(Debug, PartialEq)] +enum Step<'a> { /// Take all children out of the value (replacing temporarily with unknown) and queue them /// for processing using individual `Enter`s. - Enter(JsValue), + Enter(JsValue<'a>), /// Pop however many children there are from `done` and reinsert them into the value - Leave(JsValue), + Leave(JsValue<'a>), /// Remove the variable from `cycle_stack` which detects e.g. circular reassignments LeaveVar(Id), - LeaveLate(JsValue), + LeaveLate(JsValue<'a>), /// Call the visitor callbacks, and requeue the value for further processing if it changed. - Visit(JsValue), - EarlyVisit(JsValue), + Visit(JsValue<'a>), + EarlyVisit(JsValue<'a>), /// Remove the call from `fun_args_values` LeaveCall(u32), /// Placeholder that is used to momentarily reserve a slot that is only filled after @@ -59,7 +62,7 @@ enum Step { TemporarySlot, } -impl Display for Step { +impl Display for Step<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Step::Enter(val) => write!(f, "Enter({val})"), @@ -75,22 +78,23 @@ impl Display for Step { } // If a variable was already visited in this linking call, don't visit it again. -pub(crate) async fn link_internal_iterative<'a, B, RB, F, RF>( - graph: &'a VarGraph, - val: JsValue, - early_visitor: &'a B, - visitor: &'a F, - fun_args_values: &Mutex>>, - var_cache: &Mutex>, -) -> Result<(JsValue, u32)> +pub(crate) async fn link_internal_iterative<'a, 'l, B, RB, F, RF>( + arena: &'a ThreadLocal, + graph: &'l VarGraph<'a>, + val: JsValue<'a>, + early_visitor: &'l B, + visitor: &'l F, + fun_args_values: &Mutex>>>, + var_cache: &Mutex>>, +) -> Result<(JsValue<'a>, u32)> where - RB: 'a + Future> + Send, - B: 'a + Fn(JsValue) -> RB + Sync, - RF: 'a + Future> + Send, - F: 'a + Fn(JsValue) -> RF + Sync, + RB: 'l + Future, bool)>> + Send, + B: 'l + Fn(JsValue<'a>) -> RB + Sync, + RF: 'l + Future, bool)>> + Send, + F: 'l + Fn(JsValue<'a>) -> RF + Sync, { - let mut work_queue_stack: Vec = Vec::new(); - let mut done: Vec = Vec::new(); + let mut work_queue_stack: Vec> = Vec::new(); + let mut done: Vec> = Vec::new(); // Tracks the number of nodes in the queue and done combined let mut total_nodes = 0; let mut cycle_stack: FxHashSet = FxHashSet::default(); @@ -122,12 +126,12 @@ where .then(|| var_cache.lock()); if let Some(val) = var_cache_lock.as_deref().and_then(|cache| cache.get(&var)) { total_nodes += val.total_nodes(); - done.push(val.clone()); + done.push(val.clone_in(arena.get_or_default())); } else if let Some(value) = graph.values.get(&var) { cycle_stack.insert(var.clone()); work_queue_stack.push(Step::LeaveVar(var)); total_nodes += value.total_nodes(); - work_queue_stack.push(Step::Enter(value.clone())); + work_queue_stack.push(Step::Enter(value.clone_in(arena.get_or_default()))); } else { total_nodes += 1; done.push(JsValue::unknown( @@ -142,7 +146,9 @@ where Step::LeaveVar(var) => { cycle_stack.remove(&var); if cycle_stack.is_empty() && fun_args_values.lock().is_empty() { - var_cache.lock().insert(var, done.last().unwrap().clone()); + var_cache + .lock() + .insert(var, done.last().unwrap().clone_in(arena.get_or_default())); } } // Enter a function argument @@ -152,7 +158,7 @@ where if let Some(args) = fun_args_values.lock().get(&func_ident) { if let Some(val) = args.get(index) { total_nodes += val.total_nodes(); - done.push(val.clone()); + done.push(val.clone_in(arena.get_or_default())); } else { total_nodes += 1; done.push(JsValue::unknown_empty( @@ -176,7 +182,7 @@ where if matches!(call.callee(), JsValue::Function(..)) => { let (callee, args) = call.into_parts(); - let JsValue::Function(function_nodes, func_ident, return_value) = callee else { + let JsValue::Function(function_nodes, func_ident, mut return_value) = callee else { unreachable!() }; total_nodes -= 2; // Call + Function @@ -187,7 +193,7 @@ where } entry.insert(args); work_queue_stack.push(Step::LeaveCall(func_ident)); - work_queue_stack.push(Step::Enter(*return_value)); + work_queue_stack.push(Step::Enter(take(&mut *return_value))); } else { total_nodes -= return_value.total_nodes(); for arg in args.iter() { @@ -196,6 +202,7 @@ where total_nodes += 1; done.push(JsValue::unknown( JsValue::call_from_parts( + arena.get_or_default(), JsValue::Function(function_nodes, func_ident, return_value), args, ), @@ -222,6 +229,7 @@ where Step::Enter(mut val) => { if !val.has_children() { visit( + arena, &mut total_nodes, &mut done, &mut work_queue_stack, @@ -318,7 +326,7 @@ where done.push(JsValue::unknown_empty(true, rcstr!("node limit reached"))); continue; } - val.normalize_shallow(); + val.normalize_shallow(arena.get_or_default()); val.debug_assert_total_nodes_up_to_date(); @@ -341,7 +349,7 @@ where done.push(JsValue::unknown_empty(true, rcstr!("node limit reached"))); continue; } - val.normalize_shallow(); + val.normalize_shallow(arena.get_or_default()); val.debug_assert_total_nodes_up_to_date(); @@ -352,6 +360,7 @@ where // - visited value is put into done Step::Visit(val) => { visit( + arena, &mut total_nodes, &mut done, &mut work_queue_stack, @@ -402,22 +411,23 @@ where Ok((final_value, steps)) } -async fn visit<'a, F, RF>( +async fn visit<'a, 'l, F, RF>( + arena: &'a ThreadLocal, total_nodes: &mut u32, - done: &mut Vec, - work_queue_stack: &mut Vec, - visitor: &'a F, - val: JsValue, + done: &mut Vec>, + work_queue_stack: &mut Vec>, + visitor: &'l F, + val: JsValue<'a>, ) -> Result<()> where - RF: 'a + Future> + Send, - F: 'a + Fn(JsValue) -> RF + Sync, + RF: 'l + Future, bool)>> + Send, + F: 'l + Fn(JsValue<'a>) -> RF + Sync, { *total_nodes -= val.total_nodes(); let (mut val, visit_modified) = visitor(val).await?; if visit_modified { - val.normalize_shallow(); + val.normalize_shallow(arena.get_or_default()); #[cfg(debug_assertions)] val.debug_assert_total_nodes_up_to_date(); if val.total_nodes() > LIMIT_NODE_SIZE { diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index 2ebe220d30dd..f68900c6adcd 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -15,7 +15,10 @@ pub mod top_level_await; pub mod well_known; mod jsvalue; +pub use bump_vec::BumpVec; +pub use bumpalo::Bump; pub use jsvalue::*; +pub use thread_local::ThreadLocal; pub use well_known::{kinds::*, require_context::*}; fn is_unresolved(i: &Ident, unresolved_mark: Mark) -> bool { @@ -39,24 +42,28 @@ pub mod test_utils { }; use crate::{ analyzer::{ - RequireContextValue, builtin::replace_builtin, imports::ImportAttributes, - parse_require_context, + Bump, RequireContextValue, ThreadLocal, builtin::replace_builtin, + imports::ImportAttributes, parse_require_context, }, utils::module_value_to_well_known_object, }; - pub async fn early_visitor(mut v: JsValue) -> Result<(JsValue, bool)> { + pub async fn early_visitor<'a>( + _arena: &'a ThreadLocal, + mut v: JsValue<'a>, + ) -> Result<(JsValue<'a>, bool)> { let m = early_replace_builtin(&mut v); Ok((v, m)) } /// Visitor that replaces well known functions and objects with their /// corresponding values. Returns the new value and whether it was modified. - pub async fn visitor( - v: JsValue, + pub async fn visitor<'a>( + arena: &'a ThreadLocal, + v: JsValue<'a>, compile_time_info: Vc, attributes: &ImportAttributes, - ) -> Result<(JsValue, bool)> { + ) -> Result<(JsValue<'a>, bool)> { let ImportAttributes { ignore, .. } = *attributes; let mut new_value = match v { JsValue::Call(_, ref call) @@ -66,12 +73,13 @@ pub mod test_utils { ) => { match &call.args()[0] { - JsValue::Constant(ConstantValue::Str(v)) => { - JsValue::promise(JsValue::Module(ModuleValue { + JsValue::Constant(ConstantValue::Str(v)) => JsValue::promise( + arena.get_or_default(), + JsValue::Module(ModuleValue { module: v.as_atom().into_owned().into(), annotations: None, - })) - } + }), + ), _ => v.into_unknown(true, rcstr!("import() non constant")), } } @@ -81,13 +89,12 @@ pub mod test_utils { JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire) ) => { - if let [ - JsValue::Member( - _, - box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), - box JsValue::Constant(ConstantValue::Str(prop)), - ), - ] = call.args() + if let [JsValue::Member(_, obj, prop)] = call.args() + && matches!( + &**obj, + JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta) + ) + && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop && prop.as_str() == "url" { JsValue::WellKnownFunction(WellKnownFunctionKind::Require) @@ -152,12 +159,13 @@ pub mod test_utils { { if let [ JsValue::Constant(ConstantValue::Str(url)), - JsValue::Member( - _, - box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), - box JsValue::Constant(ConstantValue::Str(prop)), - ), + JsValue::Member(_, obj, prop), ] = call.args() + && matches!( + &**obj, + JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta) + ) + && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop { if prop.as_str() == "url" { // TODO avoid clone @@ -206,13 +214,13 @@ pub mod test_utils { } } _ => { - let (mut v, m1) = replace_well_known(v, compile_time_info, true).await?; - let m2 = replace_builtin(&mut v); + let (mut v, m1) = replace_well_known(arena, v, compile_time_info, true).await?; + let m2 = replace_builtin(arena.get_or_default(), &mut v); let m = m1 || m2 || v.make_nested_operations_unknown(); return Ok((v, m)); } }; - new_value.normalize_shallow(); + new_value.normalize_shallow(arena.get_or_default()); Ok((new_value, true)) } } @@ -251,7 +259,7 @@ mod tests { }; use crate::{ AnalyzeMode, - analyzer::{graph::AssignmentScopes, imports::ImportAttributes}, + analyzer::{Bump, ThreadLocal, graph::AssignmentScopes, imports::ImportAttributes}, }; #[fixture("tests/analyzer/graph/**/input.js")] @@ -288,6 +296,7 @@ mod tests { let cm: Arc = Arc::new(SourceMap::new(FilePathMapping::empty())); let globals = Arc::new(Globals::new()); + let arena = ThreadLocal::new(); // Keep all non-`Send` SWC types (`SingleThreadedComments`, `Lrc`) // confined to this synchronous block so they don't have to cross an `.await` @@ -317,6 +326,7 @@ mod tests { ); let var_graph = create_graph( + arena.get_or_default(), &m, &eval_context, AnalyzeMode::CodeGenerationAndTracing, @@ -328,21 +338,21 @@ mod tests { let mut named_values = var_graph .values - .clone() - .into_iter() + .iter() .map(|((id, ctx), value)| { - let unique = var_graph.values.keys().filter(|(i, _)| &id == i).count() == 1; + let unique = var_graph.values.keys().filter(|(i, _)| id == i).count() == 1; + let value = value.clone_in(arena.get_or_default()); if unique { - (id.to_string(), ((id, ctx), value)) + (id.to_string(), ((id.clone(), *ctx), value)) } else { - (format!("{id}{ctx:?}"), ((id, ctx), value)) + (format!("{id}{ctx:?}"), ((id.clone(), *ctx), value)) } }) .collect::>(); named_values.sort_by(|a, b| a.0.cmp(&b.0)); - fn explain_all<'a>( - values: impl IntoIterator)>, + fn explain_all<'x, 'a: 'x>( + values: impl IntoIterator, Option)>, ) -> String { values .into_iter() @@ -397,13 +407,14 @@ mod tests { let start = Instant::now(); let mut resolved = Vec::new(); - for (name, (id, _)) in named_values.iter().cloned() { + for (name, id) in named_values.iter().map(|(name, (id, _))| (name, id)) { let start = Instant::now(); // Ideally this would use eval_context.imports.get_attributes(span), but the // span isn't available here let (res, steps) = resolve( + &arena, &var_graph, - JsValue::Variable(id), + JsValue::Variable(id.clone()), ImportAttributes::empty_ref(), &var_cache, ) @@ -418,7 +429,7 @@ mod tests { ); } - resolved.push((name, res)); + resolved.push((name.clone(), res)); } let time = start.elapsed(); if time.as_millis() > 1 { @@ -455,28 +466,41 @@ mod tests { while let Some((parent, effect)) = queue.pop() { i += 1; let start = Instant::now(); - async fn handle_args( - args: Vec, - queue: &mut Vec<(usize, Effect)>, - var_graph: &VarGraph, - var_cache: &Mutex>, + async fn handle_args<'a>( + arena: &'a ThreadLocal, + args: Vec>, + queue: &mut Vec<(usize, Effect<'a>)>, + var_graph: &VarGraph<'a>, + var_cache: &Mutex>>, i: usize, - ) -> Vec { + ) -> Vec> { let mut new_args = Vec::with_capacity(args.len()); for arg in args { match arg { EffectArg::Value(v) => { new_args.push( - resolve(var_graph, v, ImportAttributes::empty_ref(), var_cache) - .await - .0, + resolve( + arena, + var_graph, + v, + ImportAttributes::empty_ref(), + var_cache, + ) + .await + .0, ); } EffectArg::Closure(v, effects) => { new_args.push( - resolve(var_graph, v, ImportAttributes::empty_ref(), var_cache) - .await - .0, + resolve( + arena, + var_graph, + v, + ImportAttributes::empty_ref(), + var_cache, + ) + .await + .0, ); queue.extend(effects.effects.into_iter().rev().map(|e| (i, e))); } @@ -489,11 +513,14 @@ mod tests { } let steps = match effect { Effect::Conditional { - condition, kind, .. + mut condition, + kind, + .. } => { let (condition, steps) = resolve( + &arena, &var_graph, - *condition, + take(&mut *condition), ImportAttributes::empty_ref(), &var_cache, ) @@ -529,27 +556,28 @@ mod tests { steps } Effect::Call { - func, + mut func, args, new, span, .. } => { let (func, steps) = resolve( + &arena, &var_graph, - *func, + take(&mut *func), eval_context.imports.get_attributes(span), &var_cache, ) .await; let new_args = - handle_args(args, &mut queue, &var_graph, &var_cache, i).await; + handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await; resolved.push(( format!("{parent} -> {i} call"), if new { - JsValue::new_from_iter(func, new_args) + JsValue::new_from_iter(arena.get_or_default(), func, new_args) } else { - JsValue::call_from_iter(func, new_args) + JsValue::call_from_iter(arena.get_or_default(), func, new_args) }, )); steps @@ -558,39 +586,66 @@ mod tests { resolved.push((format!("{parent} -> {i} free var"), JsValue::FreeVar(var))); 0 } - Effect::TypeOf { arg, .. } => { - let (arg, steps) = - resolve(&var_graph, *arg, ImportAttributes::empty_ref(), &var_cache) - .await; + Effect::TypeOf { mut arg, .. } => { + let (arg, steps) = resolve( + &arena, + &var_graph, + take(&mut *arg), + ImportAttributes::empty_ref(), + &var_cache, + ) + .await; resolved.push(( format!("{parent} -> {i} typeof"), - JsValue::type_of(Box::new(arg)), + JsValue::type_of(arena.get_or_default(), arg), )); steps } Effect::MemberCall { - obj, prop, args, .. + mut obj, + mut prop, + args, + .. } => { - let (obj, obj_steps) = - resolve(&var_graph, *obj, ImportAttributes::empty_ref(), &var_cache) - .await; - let (prop, prop_steps) = - resolve(&var_graph, *prop, ImportAttributes::empty_ref(), &var_cache) - .await; + let (obj, obj_steps) = resolve( + &arena, + &var_graph, + take(&mut *obj), + ImportAttributes::empty_ref(), + &var_cache, + ) + .await; + let (prop, prop_steps) = resolve( + &arena, + &var_graph, + take(&mut *prop), + ImportAttributes::empty_ref(), + &var_cache, + ) + .await; let new_args = - handle_args(args, &mut queue, &var_graph, &var_cache, i).await; + handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await; resolved.push(( format!("{parent} -> {i} member call"), - JsValue::member_call_from_iter(obj, prop, new_args), + JsValue::member_call_from_iter( + arena.get_or_default(), + obj, + prop, + new_args, + ), )); obj_steps + prop_steps } Effect::DynamicImport { args, .. } => { let new_args = - handle_args(args, &mut queue, &var_graph, &var_cache, i).await; + handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await; resolved.push(( format!("{parent} -> {i} dynamic import"), - JsValue::call_from_iter(JsValue::FreeVar("import".into()), new_args), + JsValue::call_from_iter( + arena.get_or_default(), + JsValue::FreeVar("import".into()), + new_args, + ), )); 0 } @@ -643,12 +698,13 @@ mod tests { Ok(()) } - async fn resolve( - var_graph: &VarGraph, - val: JsValue, + async fn resolve<'a>( + arena: &'a ThreadLocal, + var_graph: &VarGraph<'a>, + val: JsValue<'a>, attributes: &ImportAttributes, - var_cache: &Mutex>, - ) -> (JsValue, u32) { + var_cache: &Mutex>>, + ) -> (JsValue<'a>, u32) { // The caller (`fixture`) runs us inside `tt.run_once`, so a real // turbo-tasks task context is already established here. async { @@ -673,11 +729,13 @@ mod tests { .cell() .await?; link( + arena, var_graph, val, - &super::test_utils::early_visitor, + &(|val| Box::pin(super::test_utils::early_visitor(arena, val))), &(|val| { Box::pin(super::test_utils::visitor( + arena, val, compile_time_info, attributes, diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/kinds.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/kinds.rs index ac5d9b74bfbb..d56ceb840858 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/kinds.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/kinds.rs @@ -143,7 +143,7 @@ impl WellKnownObjectKind { /// A list of well-known functions that have special meaning in the analysis. #[derive(Debug, Clone, Hash, PartialEq)] -pub enum WellKnownFunctionKind { +pub enum WellKnownFunctionKind<'a> { ArrayFilter, ArrayForEach, ArrayMap, @@ -151,7 +151,7 @@ pub enum WellKnownFunctionKind { PathJoin, PathDirname, /// `0` is the current working directory. - PathResolve(Box), + PathResolve(&'a JsValue<'a>), Import, Require, /// `0` is the path to resolve from (relative to the current module). @@ -196,7 +196,7 @@ pub enum WellKnownFunctionKind { ImportMetaGlob, } -impl WellKnownFunctionKind { +impl WellKnownFunctionKind<'_> { pub fn as_define_name(&self) -> Option<&[&str]> { match self { Self::Import { .. } => Some(&["import"]), diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs index d581aaaeffba..46b9c899f268 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs @@ -12,13 +12,14 @@ use url::Url; use super::{ ConstantValue, JsValue, JsValueUrlKind, ModuleValue, WellKnownFunctionKind, WellKnownObjectKind, }; -use crate::analyzer::RequireContextValue; +use crate::analyzer::{Bump, BumpVec, RequireContextValue, ThreadLocal}; -pub async fn replace_well_known( - value: JsValue, +pub async fn replace_well_known<'a>( + arena: &'a ThreadLocal, + value: JsValue<'a>, compile_time_info: Vc, allow_project_root_tracing: bool, -) -> Result<(JsValue, bool)> { +) -> Result<(JsValue<'a>, bool)> { Ok(match value { JsValue::Call(_, call) if matches!(call.callee(), JsValue::WellKnownFunction(_)) => { let (callee, args) = call.into_parts(); @@ -27,6 +28,7 @@ pub async fn replace_well_known( }; ( well_known_function_call( + arena, kind, JsValue::unknown_empty(false, rcstr!("this is not analyzed yet")), args, @@ -43,34 +45,45 @@ pub async fn replace_well_known( if call.args().len() == 1 && let JsValue::WellKnownObject(_) = &call.args()[0] { - return Ok((call.args()[0].clone(), true)); + return Ok((call.args()[0].clone_in(arena.get_or_default()), true)); } (JsValue::Call(total, call), false) } - JsValue::Member(_, box JsValue::WellKnownObject(kind), box prop) => { - well_known_object_member(kind, prop, compile_time_info).await? + JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::WellKnownObject(_)) => { + let JsValue::WellKnownObject(kind) = take(&mut *obj) else { + unreachable!() + }; + well_known_object_member(arena, kind, take(&mut *prop), compile_time_info).await? } - JsValue::Member(_, box JsValue::WellKnownFunction(kind), box prop) => { - well_known_function_member(kind, prop) + JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::WellKnownFunction(_)) => { + let JsValue::WellKnownFunction(kind) = take(&mut *obj) else { + unreachable!() + }; + well_known_function_member(arena.get_or_default(), kind, take(&mut *prop)) + } + JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::Array { .. }) => { + match prop.as_str() { + Some("filter") => ( + JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayFilter), + true, + ), + Some("forEach") => ( + JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayForEach), + true, + ), + Some("map") => ( + JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayMap), + true, + ), + _ => ( + JsValue::member(arena.get_or_default(), take(&mut *obj), take(&mut *prop)), + false, + ), + } } - JsValue::Member(_, box JsValue::Array { .. }, box ref prop) => match prop.as_str() { - Some("filter") => ( - JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayFilter), - true, - ), - Some("forEach") => ( - JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayForEach), - true, - ), - Some("map") => ( - JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayMap), - true, - ), - _ => (value, false), - }, // module.hot → WellKnownObject(ModuleHot) (only when HMR is enabled) - JsValue::Member(_, box JsValue::FreeVar(ref name), box ref prop) - if &**name == "module" + JsValue::Member(_, obj, prop) + if matches!(&*obj, JsValue::FreeVar(name) if &**name == "module") && prop.as_str() == Some("hot") && compile_time_info.await?.hot_module_replacement_enabled => { @@ -83,30 +96,35 @@ pub async fn replace_well_known( }) } -pub async fn well_known_function_call( - kind: WellKnownFunctionKind, - _this: JsValue, - args: Vec, +pub async fn well_known_function_call<'a>( + arena: &'a ThreadLocal, + kind: WellKnownFunctionKind<'a>, + _this: JsValue<'a>, + args: BumpVec<'a, JsValue<'a>>, compile_time_info: Vc, allow_project_root_tracing: bool, -) -> Result { +) -> Result> { Ok(match kind { - WellKnownFunctionKind::ObjectAssign => object_assign(args), - WellKnownFunctionKind::PathJoin => path_join(args), - WellKnownFunctionKind::PathDirname => path_dirname(args), - WellKnownFunctionKind::PathResolve(cwd) => path_resolve(*cwd, args), - WellKnownFunctionKind::Import => import(args), - WellKnownFunctionKind::Require => require(args), + WellKnownFunctionKind::ObjectAssign => object_assign(arena.get_or_default(), args), + WellKnownFunctionKind::PathJoin => path_join(arena.get_or_default(), args), + WellKnownFunctionKind::PathDirname => path_dirname(arena.get_or_default(), args), + WellKnownFunctionKind::PathResolve(cwd) => path_resolve( + arena.get_or_default(), + cwd.clone_in(arena.get_or_default()), + args, + ), + WellKnownFunctionKind::Import => import(arena.get_or_default(), args), + WellKnownFunctionKind::Require => require(arena.get_or_default(), args), WellKnownFunctionKind::RequireContextRequire(value) => { - require_context_require(value, args)? + require_context_require(arena.get_or_default(), value, args)? } WellKnownFunctionKind::RequireContextRequireKeys(value) => { - require_context_require_keys(value, args)? + require_context_require_keys(arena.get_or_default(), value, args)? } WellKnownFunctionKind::RequireContextRequireResolve(value) => { - require_context_require_resolve(value, args)? + require_context_require_resolve(arena.get_or_default(), value, args)? } - WellKnownFunctionKind::PathToFileUrl => path_to_file_url(args), + WellKnownFunctionKind::PathToFileUrl => path_to_file_url(arena.get_or_default(), args), WellKnownFunctionKind::OsArch => compile_time_info .environment() .compile_target() @@ -128,7 +146,11 @@ pub async fn well_known_function_call( format!("/ROOT/{}", cwd.path).into() } else { JsValue::unknown( - JsValue::call_from_parts(JsValue::WellKnownFunction(kind), args), + JsValue::call_from_parts( + arena.get_or_default(), + JsValue::WellKnownFunction(kind), + args, + ), true, rcstr!("process.cwd is not specified in the environment"), ) @@ -150,14 +172,18 @@ pub async fn well_known_function_call( } _ => JsValue::unknown( - JsValue::call_from_parts(JsValue::WellKnownFunction(kind), args), + JsValue::call_from_parts( + arena.get_or_default(), + JsValue::WellKnownFunction(kind), + args, + ), true, rcstr!("unsupported function"), ), }) } -fn object_assign(args: Vec) -> JsValue { +fn object_assign<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { if args.iter().all(|arg| matches!(arg, JsValue::Object { .. })) { if let Some(mut merged_object) = args.into_iter().reduce(|mut acc, cur| { if let JsValue::Object { parts, mutable, .. } = &mut acc @@ -167,7 +193,7 @@ fn object_assign(args: Vec) -> JsValue { .. } = &cur { - parts.extend_from_slice(next_parts); + parts.extend(arena, next_parts.iter().map(|p| p.clone_in(arena))); *mutable |= *next_mutable; } acc @@ -177,6 +203,7 @@ fn object_assign(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_iter( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign), [], ), @@ -187,6 +214,7 @@ fn object_assign(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign), args, ), @@ -196,11 +224,11 @@ fn object_assign(args: Vec) -> JsValue { } } -fn path_join(args: Vec) -> JsValue { +fn path_join<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { if args.is_empty() { return rcstr!(".").into(); } - let mut parts = Vec::new(); + let mut parts: Vec> = Vec::new(); for item in args { if let Some(str) = item.as_str() { let split = str.split('/'); @@ -209,8 +237,8 @@ fn path_join(args: Vec) -> JsValue { parts.push(item); } } - let mut results_final = Vec::new(); - let mut results: Vec = Vec::new(); + let mut results_final: Vec> = Vec::new(); + let mut results: Vec> = Vec::new(); for item in parts { if let Some(str) = item.as_str() { match str { @@ -241,18 +269,22 @@ fn path_join(args: Vec) -> JsValue { if last_is_str && is_str { results.push(rcstr!("/").into()); } else { - results.push(JsValue::alternatives(vec![ - rcstr!("/").into(), - rcstr!("").into(), - ])); + results.push(JsValue::alternatives(BumpVec::from_iter_in( + arena, + [rcstr!("/").into(), rcstr!("").into()], + ))); } results.push(part); last_is_str = is_str; } - JsValue::concat(results) + JsValue::concat(BumpVec::from_iter_in(arena, results)) } -fn path_resolve(cwd: JsValue, mut args: Vec) -> JsValue { +fn path_resolve<'a>( + arena: &'a Bump, + cwd: JsValue<'a>, + mut args: BumpVec<'a, JsValue<'a>>, +) -> JsValue<'a> { // If no path segments are passed, `path.resolve()` will return the absolute // path of the current working directory. if args.is_empty() { @@ -268,12 +300,12 @@ fn path_resolve(cwd: JsValue, mut args: Vec) -> JsValue { && let Some(str) = arg.as_str() && str.starts_with('/') { - return path_resolve(cwd, args.drain(idx..).collect()); + return path_resolve(arena, cwd, args.split_off(arena, idx)); } } - let mut results_final = Vec::new(); - let mut results: Vec = Vec::new(); + let mut results_final: Vec> = Vec::new(); + let mut results: Vec> = Vec::new(); for item in args { if let Some(str) = item.as_str() { for str in str.split('/') { @@ -315,19 +347,19 @@ fn path_resolve(cwd: JsValue, mut args: Vec) -> JsValue { if last_was_str && is_str { results.push(rcstr!("/").into()); } else { - results.push(JsValue::alternatives(vec![ - rcstr!("/").into(), - rcstr!("").into(), - ])); + results.push(JsValue::alternatives(BumpVec::from_iter_in( + arena, + [rcstr!("/").into(), rcstr!("").into()], + ))); } results.push(part); last_was_str = is_str; } - JsValue::concat(results) + JsValue::concat(BumpVec::from_iter_in(arena, results)) } -fn path_dirname(mut args: Vec) -> JsValue { +fn path_dirname<'a>(arena: &'a Bump, mut args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { if let Some(arg) = args.iter_mut().next() { if let Some(str) = arg.as_str() { if let Some(i) = str.rfind('/') { @@ -346,6 +378,7 @@ fn path_dirname(mut args: Vec) -> JsValue { } JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::PathDirname), args, ), @@ -356,16 +389,18 @@ fn path_dirname(mut args: Vec) -> JsValue { /// Resolve the contents of an import call, throwing errors /// if we come across any unsupported syntax. -pub fn import(args: Vec) -> JsValue { +pub fn import<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { match &args[..] { - [JsValue::Constant(ConstantValue::Str(v))] => { - JsValue::promise(JsValue::Module(ModuleValue { + [JsValue::Constant(ConstantValue::Str(v))] => JsValue::promise( + arena, + JsValue::Module(ModuleValue { module: v.as_atom().into_owned().into(), annotations: None, - })) - } + }), + ), _ => JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::Import), args, ), @@ -377,7 +412,7 @@ pub fn import(args: Vec) -> JsValue { /// Resolve the contents of a require call, throwing errors /// if we come across any unsupported syntax. -fn require(args: Vec) -> JsValue { +fn require<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { if args.len() == 1 { if let Some(s) = args[0].as_str() { JsValue::Module(ModuleValue { @@ -387,6 +422,7 @@ fn require(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::Require), args, ), @@ -397,6 +433,7 @@ fn require(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::Require), args, ), @@ -407,10 +444,15 @@ fn require(args: Vec) -> JsValue { } /// (try to) statically evaluate `require.context(...)()` -fn require_context_require(val: Box, args: Vec) -> Result { +fn require_context_require<'a>( + arena: &'a Bump, + val: Box, + args: BumpVec<'a, JsValue<'a>>, +) -> Result> { if args.is_empty() { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)), args, ), @@ -424,6 +466,7 @@ fn require_context_require(val: Box, args: Vec) -> let Some(s) = args[0].as_str() else { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)), args, ), @@ -437,6 +480,7 @@ fn require_context_require(val: Box, args: Vec) -> let Some(m) = val.0.get(s) else { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)), args, ), @@ -455,15 +499,20 @@ fn require_context_require(val: Box, args: Vec) -> } /// (try to) statically evaluate `require.context(...).keys()` -fn require_context_require_keys( +fn require_context_require_keys<'a>( + arena: &'a Bump, val: Box, - args: Vec, -) -> Result { + args: BumpVec<'a, JsValue<'a>>, +) -> Result> { Ok(if args.is_empty() { - JsValue::array(val.0.keys().cloned().map(|k| k.into()).collect()) + JsValue::array(BumpVec::from_iter_in( + arena, + val.0.keys().cloned().map(|k| k.into()), + )) } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireKeys(val)), args, ), @@ -474,13 +523,15 @@ fn require_context_require_keys( } /// (try to) statically evaluate `require.context(...).resolve()` -fn require_context_require_resolve( +fn require_context_require_resolve<'a>( + arena: &'a Bump, val: Box, - args: Vec, -) -> Result { + args: BumpVec<'a, JsValue<'a>>, +) -> Result> { if args.len() != 1 { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve( val, )), @@ -496,6 +547,7 @@ fn require_context_require_resolve( let Some(s) = args[0].as_str() else { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve( val, )), @@ -511,6 +563,7 @@ fn require_context_require_resolve( let Some(m) = val.0.get(s) else { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve( val, )), @@ -527,7 +580,7 @@ fn require_context_require_resolve( Ok(m.as_str().into()) } -fn path_to_file_url(args: Vec) -> JsValue { +fn path_to_file_url<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> { if args.len() == 1 { if let Some(path) = args[0].as_str() { Url::from_file_path(path) @@ -535,6 +588,7 @@ fn path_to_file_url(args: Vec) -> JsValue { .unwrap_or_else(|_| { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl), args, ), @@ -545,6 +599,7 @@ fn path_to_file_url(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl), args, ), @@ -555,6 +610,7 @@ fn path_to_file_url(args: Vec) -> JsValue { } else { JsValue::unknown( JsValue::call_from_parts( + arena, JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl), args, ), @@ -564,7 +620,11 @@ fn path_to_file_url(args: Vec) -> JsValue { } } -fn well_known_function_member(kind: WellKnownFunctionKind, prop: JsValue) -> (JsValue, bool) { +fn well_known_function_member<'a>( + arena: &'a Bump, + kind: WellKnownFunctionKind<'a>, + prop: JsValue<'a>, +) -> (JsValue<'a>, bool) { let new_value = match (kind, prop.as_str()) { (WellKnownFunctionKind::Require, Some("resolve")) => { JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve) @@ -593,7 +653,7 @@ fn well_known_function_member(kind: WellKnownFunctionKind, prop: JsValue) -> (Js #[allow(unreachable_patterns)] (kind, _) => { return ( - JsValue::member(Box::new(JsValue::WellKnownFunction(kind)), Box::new(prop)), + JsValue::member(arena, JsValue::WellKnownFunction(kind), prop), false, ); } @@ -601,43 +661,48 @@ fn well_known_function_member(kind: WellKnownFunctionKind, prop: JsValue) -> (Js (new_value, true) } -async fn well_known_object_member( +async fn well_known_object_member<'a>( + arena: &'a ThreadLocal, kind: WellKnownObjectKind, - prop: JsValue, + prop: JsValue<'a>, compile_time_info: Vc, -) -> Result<(JsValue, bool)> { +) -> Result<(JsValue<'a>, bool)> { let new_value = match kind { - WellKnownObjectKind::GlobalObject => global_object(prop), + WellKnownObjectKind::GlobalObject => global_object(arena.get_or_default(), prop), WellKnownObjectKind::PathModule | WellKnownObjectKind::PathModuleDefault => { - path_module_member(kind, prop, compile_time_info).await? + path_module_member(arena, kind, prop, compile_time_info).await? } WellKnownObjectKind::FsModule | WellKnownObjectKind::FsModuleDefault - | WellKnownObjectKind::FsModulePromises => fs_module_member(kind, prop), + | WellKnownObjectKind::FsModulePromises => { + fs_module_member(arena.get_or_default(), kind, prop) + } WellKnownObjectKind::FsExtraModule | WellKnownObjectKind::FsExtraModuleDefault => { - fs_extra_module_member(kind, prop) + fs_extra_module_member(arena.get_or_default(), kind, prop) } WellKnownObjectKind::ModuleModule | WellKnownObjectKind::ModuleModuleDefault => { - module_module_member(kind, prop) + module_module_member(arena.get_or_default(), kind, prop) } WellKnownObjectKind::UrlModule | WellKnownObjectKind::UrlModuleDefault => { - url_module_member(kind, prop) + url_module_member(arena.get_or_default(), kind, prop) } WellKnownObjectKind::WorkerThreadsModule | WellKnownObjectKind::WorkerThreadsModuleDefault => { - worker_threads_module_member(kind, prop) + worker_threads_module_member(arena.get_or_default(), kind, prop) } WellKnownObjectKind::ChildProcessModule - | WellKnownObjectKind::ChildProcessModuleDefault => child_process_module_member(kind, prop), + | WellKnownObjectKind::ChildProcessModuleDefault => { + child_process_module_member(arena.get_or_default(), kind, prop) + } WellKnownObjectKind::OsModule | WellKnownObjectKind::OsModuleDefault => { - os_module_member(kind, prop) + os_module_member(arena.get_or_default(), kind, prop) } WellKnownObjectKind::NodeProcessModule => { - node_process_member(prop, compile_time_info).await? + node_process_member(arena, prop, compile_time_info).await? } - WellKnownObjectKind::NodePreGyp => node_pre_gyp(prop), - WellKnownObjectKind::NodeExpressApp => express(prop), - WellKnownObjectKind::NodeProtobufLoader => protobuf_loader(prop), + WellKnownObjectKind::NodePreGyp => node_pre_gyp(arena.get_or_default(), prop), + WellKnownObjectKind::NodeExpressApp => express(arena.get_or_default(), prop), + WellKnownObjectKind::NodeProtobufLoader => protobuf_loader(arena.get_or_default(), prop), WellKnownObjectKind::ImportMeta => match prop.as_str() { // import.meta.turbopackHot is the ESM equivalent of module.hot for HMR Some("turbopackHot") if compile_time_info.await?.hot_module_replacement_enabled => { @@ -649,7 +714,7 @@ async fn well_known_object_member( Some("glob") => JsValue::WellKnownFunction(WellKnownFunctionKind::ImportMetaGlob), _ => { return Ok(( - JsValue::member(Box::new(JsValue::WellKnownObject(kind)), Box::new(prop)), + JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop), false, )); } @@ -660,7 +725,11 @@ async fn well_known_object_member( _ => { return Ok(( JsValue::unknown( - JsValue::member(Box::new(JsValue::WellKnownObject(kind)), Box::new(prop)), + JsValue::member( + arena.get_or_default(), + JsValue::WellKnownObject(kind), + prop, + ), true, rcstr!("unsupported property on module.hot"), ), @@ -671,7 +740,7 @@ async fn well_known_object_member( #[allow(unreachable_patterns)] _ => { return Ok(( - JsValue::member(Box::new(JsValue::WellKnownObject(kind)), Box::new(prop)), + JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop), false, )); } @@ -679,13 +748,14 @@ async fn well_known_object_member( Ok((new_value, true)) } -fn global_object(prop: JsValue) -> JsValue { +fn global_object<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> { match prop.as_str() { Some("assign") => JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign), _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject), + prop, ), true, rcstr!("unsupported property on global Object"), @@ -693,19 +763,20 @@ fn global_object(prop: JsValue) -> JsValue { } } -async fn path_module_member( +async fn path_module_member<'a>( + arena: &'a ThreadLocal, kind: WellKnownObjectKind, - prop: JsValue, + prop: JsValue<'a>, compile_time_info: Vc, -) -> Result { +) -> Result> { Ok(match (kind, prop.as_str()) { (.., Some("join")) => JsValue::WellKnownFunction(WellKnownFunctionKind::PathJoin), (.., Some("dirname")) => JsValue::WellKnownFunction(WellKnownFunctionKind::PathDirname), (.., Some("resolve")) => { // cwd is added while resolving in references.rs - JsValue::WellKnownFunction(WellKnownFunctionKind::PathResolve(Box::new(JsValue::from( - "", - )))) + JsValue::WellKnownFunction(WellKnownFunctionKind::PathResolve( + arena.get_or_default().alloc(JsValue::from("")), + )) } (.., Some("sep")) => compile_time_info .environment() @@ -719,8 +790,9 @@ async fn path_module_member( } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::PathModule)), - Box::new(prop), + arena.get_or_default(), + JsValue::WellKnownObject(WellKnownObjectKind::PathModule), + prop, ), true, rcstr!("unsupported property on Node.js path module"), @@ -728,7 +800,11 @@ async fn path_module_member( }) } -fn fs_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn fs_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { if let Some(word) = prop.as_str() { match (kind, word) { ( @@ -754,15 +830,20 @@ fn fs_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::FsModule)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::FsModule), + prop, ), true, rcstr!("unsupported property on Node.js fs module"), ) } -fn fs_extra_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn fs_extra_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { if let Some(word) = prop.as_str() { match (kind, word) { // regular fs methods @@ -793,15 +874,20 @@ fn fs_extra_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule), + prop, ), true, rcstr!("unsupported property on fs-extra module"), ) } -fn module_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn module_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { match (kind, prop.as_str()) { (.., Some("createRequire")) => { JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire) @@ -811,8 +897,9 @@ fn module_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::ModuleModule)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::ModuleModule), + prop, ), true, rcstr!("unsupported property on Node.js `module` module"), @@ -820,7 +907,11 @@ fn module_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } } -fn url_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn url_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { match (kind, prop.as_str()) { (.., Some("pathToFileURL")) => { JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl) @@ -830,8 +921,9 @@ fn url_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::UrlModule)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::UrlModule), + prop, ), true, rcstr!("unsupported property on Node.js url module"), @@ -839,7 +931,11 @@ fn url_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } } -fn worker_threads_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn worker_threads_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { match (kind, prop.as_str()) { (.., Some("Worker")) => { JsValue::WellKnownFunction(WellKnownFunctionKind::NodeWorkerConstructor) @@ -849,10 +945,9 @@ fn worker_threads_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsV } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject( - WellKnownObjectKind::WorkerThreadsModule, - )), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::WorkerThreadsModule), + prop, ), true, rcstr!("unsupported property on Node.js worker_threads module"), @@ -860,7 +955,11 @@ fn worker_threads_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsV } } -fn child_process_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn child_process_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { let prop_str = prop.as_str(); match (kind, prop_str) { (.., Some("spawn" | "spawnSync" | "execFile" | "execFileSync")) => { @@ -875,10 +974,9 @@ fn child_process_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsVa _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject( - WellKnownObjectKind::ChildProcessModule, - )), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::ChildProcessModule), + prop, ), true, rcstr!("unsupported property on Node.js child_process module"), @@ -886,7 +984,11 @@ fn child_process_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsVa } } -fn os_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { +fn os_module_member<'a>( + arena: &'a Bump, + kind: WellKnownObjectKind, + prop: JsValue<'a>, +) -> JsValue<'a> { match (kind, prop.as_str()) { (.., Some("platform")) => JsValue::WellKnownFunction(WellKnownFunctionKind::OsPlatform), (.., Some("arch")) => JsValue::WellKnownFunction(WellKnownFunctionKind::OsArch), @@ -896,8 +998,9 @@ fn os_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::OsModule)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::OsModule), + prop, ), true, rcstr!("unsupported property on Node.js os module"), @@ -905,10 +1008,11 @@ fn os_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue { } } -async fn node_process_member( - prop: JsValue, +async fn node_process_member<'a>( + arena: &'a ThreadLocal, + prop: JsValue<'a>, compile_time_info: Vc, -) -> Result { +) -> Result> { Ok(match prop.as_str() { Some("arch") => compile_time_info .environment() @@ -929,10 +1033,9 @@ async fn node_process_member( Some("env") => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessEnv), _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject( - WellKnownObjectKind::NodeProcessModule, - )), - Box::new(prop), + arena.get_or_default(), + JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule), + prop, ), true, rcstr!("unsupported property on Node.js process object"), @@ -940,13 +1043,14 @@ async fn node_process_member( }) } -fn node_pre_gyp(prop: JsValue) -> JsValue { +fn node_pre_gyp<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> { match prop.as_str() { Some("find") => JsValue::WellKnownFunction(WellKnownFunctionKind::NodePreGypFind), _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject(WellKnownObjectKind::NodePreGyp)), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::NodePreGyp), + prop, ), true, rcstr!("unsupported property on @mapbox/node-pre-gyp module"), @@ -954,15 +1058,14 @@ fn node_pre_gyp(prop: JsValue) -> JsValue { } } -fn express(prop: JsValue) -> JsValue { +fn express<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> { match prop.as_str() { Some("set") => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeExpressSet), _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject( - WellKnownObjectKind::NodeExpressApp, - )), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::NodeExpressApp), + prop, ), true, rcstr!("unsupported property on require('express')() object"), @@ -970,17 +1073,16 @@ fn express(prop: JsValue) -> JsValue { } } -fn protobuf_loader(prop: JsValue) -> JsValue { +fn protobuf_loader<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> { match prop.as_str() { Some("load") | Some("loadSync") => { JsValue::WellKnownFunction(WellKnownFunctionKind::NodeProtobufLoad) } _ => JsValue::unknown( JsValue::member( - Box::new(JsValue::WellKnownObject( - WellKnownObjectKind::NodeProtobufLoader, - )), - Box::new(prop), + arena, + JsValue::WellKnownObject(WellKnownObjectKind::NodeProtobufLoader), + prop, ), true, rcstr!("unsupported property on require('@grpc/proto-loader') object"), diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/require_context.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/require_context.rs index 7e2076400f83..583185ac93af 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/require_context.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/require_context.rs @@ -23,7 +23,7 @@ pub struct RequireContextOptions { /// Parse the arguments passed to a require.context invocation, validate them /// and convert them to the appropriate rust values. -pub fn parse_require_context(args: &[JsValue]) -> Result { +pub fn parse_require_context(args: &[JsValue<'_>]) -> Result { if !(1..=3).contains(&args.len()) { // https://linear.app/vercel/issue/WEB-910/add-support-for-requirecontexts-mode-argument bail!("require.context() only supports 1-3 arguments (mode is not supported)"); diff --git a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs index a0545f4c4f39..2c11a65d7644 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs @@ -88,7 +88,7 @@ pub struct ImportMetaGlobOptions { /// - **`as` option** (deprecated in Vite 5 in favor of `query`) is not supported. Use `query: /// '?raw'` or `query: '?url'` instead. pub fn parse_import_meta_glob( - args: &[JsValue], + args: &[JsValue<'_>], handler: &Handler, span: Span, diagnostic_id: DiagnosticId, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index a2335d8d8220..8db2817e3a65 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -98,8 +98,9 @@ use crate::{ AnalyzeMode, EcmascriptModuleAsset, EcmascriptModuleAssetType, EcmascriptParsable, ModuleTypeResult, TreeShakingMode, TypeofWindow, analyzer::{ - ConstantNumber, ConstantString, ConstantValue as JsConstantValue, JsValue, JsValueUrlKind, - ObjectPart, RequireContextValue, WellKnownFunctionKind, WellKnownObjectKind, + Bump, BumpVec, ConstantNumber, ConstantString, ConstantValue as JsConstantValue, JsValue, + JsValueUrlKind, ObjectPart, RequireContextValue, ThreadLocal, WellKnownFunctionKind, + WellKnownObjectKind, builtin::{early_replace_builtin, replace_builtin}, graph::{ConditionalKind, Effect, EffectArg, VarGraph, create_graph}, imports::{ImportAnnotations, ImportAttributes, ImportMap}, @@ -438,15 +439,16 @@ struct AnalysisState<'a> { compile_time_info: ResolvedVc, free_var_references_members: ResolvedVc, compile_time_info_ref: ReadRef, - var_graph: &'a VarGraph, + arena: &'a ThreadLocal, + var_graph: VarGraph<'a>, /// Whether to allow tracing to reference files from the project root. This is used to prevent /// random node_modules packages from tracing the entire project due to some dynamic /// `path.join(foo, bar)` call. allow_project_root_tracing: bool, /// This is the current state of known values of function /// arguments. - fun_args_values: Mutex>>, - var_cache: Mutex>, + fun_args_values: Mutex>>>, + var_cache: Mutex>>, // There can be many references to import.meta, but only the first should hoist // the object allocation. first_import_meta: bool, @@ -473,20 +475,26 @@ struct AnalysisState<'a> { inner_assets: Option>, } -impl AnalysisState<'_> { +impl<'a> AnalysisState<'a> { /// Links a value to the graph, returning the linked value. - async fn link_value(&self, value: JsValue, attributes: &ImportAttributes) -> Result { + async fn link_value( + &self, + value: JsValue<'a>, + attributes: &ImportAttributes, + ) -> Result> { Ok(link( - self.var_graph, + self.arena, + &self.var_graph, value, - &early_value_visitor, + &|value| early_value_visitor(self.arena, value), &|value| { value_visitor( + self.arena, *self.origin, value, *self.compile_time_info, &self.compile_time_info_ref, - self.var_graph, + &self.var_graph, attributes, self.allow_project_root_tracing, ) @@ -797,11 +805,23 @@ async fn analyze_ecmascript_module_internal( .instrument(span) .await?; + // The arena that owns every `JsValue` built during this analysis. Borrowed once here so all + // uses share a single (covariant) reference lifetime; it is freed when the function returns. + let arena = ThreadLocal::new(); + let arena = &arena; let mut var_graph = { let _span = tracing::trace_span!("analyze variable values").entered(); + let mut graph = None; set_handler_and_globals(&handler, globals, || { - create_graph(program, eval_context, analyze_mode, supports_block_scoping) - }) + graph = Some(create_graph( + arena.get_or_default(), + program, + eval_context, + analyze_mode, + supports_block_scoping, + )); + }); + graph.unwrap() }; let span = tracing::trace_span!("effects processing"); @@ -811,6 +831,7 @@ async fn analyze_ecmascript_module_internal( let compile_time_info_ref = compile_time_info.await?; let mut analysis_state = AnalysisState { + arena, handler: &handler, module, source, @@ -822,7 +843,7 @@ async fn analyze_ecmascript_module_internal( .to_resolved() .await?, compile_time_info_ref, - var_graph: &var_graph, + var_graph, allow_project_root_tracing: !source.ident().await?.path.is_in_node_modules(), fun_args_values: Default::default(), var_cache: Default::default(), @@ -840,8 +861,8 @@ async fn analyze_ecmascript_module_internal( inner_assets, }; - enum Action { - Effect(Effect), + enum Action<'a> { + Effect(Effect<'a>), LeaveScope(u32), } @@ -863,7 +884,7 @@ async fn analyze_ecmascript_module_internal( Action::Effect(effect) => effect, }; - let add_effects = |effects: Vec| { + let add_effects = |effects: Vec<_>| { queue_stack .lock() .extend(effects.into_iter().map(Action::Effect).rev()) @@ -880,7 +901,7 @@ async fn analyze_ecmascript_module_internal( .add_code_gen(Unreachable::new(AstPathRange::StartAfter(start_ast_path))); } Effect::Conditional { - condition, + mut condition, kind, ast_path: condition_ast_path, span: _, @@ -890,7 +911,7 @@ async fn analyze_ecmascript_module_internal( let condition_has_side_effects = condition.has_side_effects(); let condition = analysis_state - .link_value(*condition, ImportAttributes::empty_ref()) + .link_value(take(&mut *condition), ImportAttributes::empty_ref()) .await?; macro_rules! inactive { @@ -1039,7 +1060,7 @@ async fn analyze_ecmascript_module_internal( } } Effect::Call { - func, + mut func, args, ast_path, span, @@ -1047,7 +1068,7 @@ async fn analyze_ecmascript_module_internal( new, } => { let func = analysis_state - .link_value(*func, eval_context.imports.get_attributes(span)) + .link_value(take(&mut *func), eval_context.imports.get_attributes(span)) .await?; handle_call( @@ -1085,8 +1106,8 @@ async fn analyze_ecmascript_module_internal( .await?; } Effect::MemberCall { - obj, - prop, + mut obj, + mut prop, mut args, ast_path, span, @@ -1095,7 +1116,11 @@ async fn analyze_ecmascript_module_internal( } => { let func = analysis_state .link_value( - JsValue::member(obj.clone(), prop), + JsValue::member( + arena.get_or_default(), + obj.clone_in(arena.get_or_default()), + take(&mut *prop), + ), eval_context.imports.get_attributes(span), ) .await?; @@ -1115,7 +1140,7 @@ async fn analyze_ecmascript_module_internal( mutable, .. } = analysis_state - .link_value(*obj, eval_context.imports.get_attributes(span)) + .link_value(take(&mut *obj), eval_context.imports.get_attributes(span)) .await? { *value = analysis_state @@ -1124,12 +1149,12 @@ async fn analyze_ecmascript_module_internal( if let JsValue::Function(_, func_ident, _) = value { let mut closure_arg = JsValue::alternatives(take(values)); if mutable { - closure_arg.add_unknown_mutations(true); + closure_arg.add_unknown_mutations(arena.get_or_default(), true); } - analysis_state - .fun_args_values - .get_mut() - .insert(*func_ident, vec![closure_arg]); + analysis_state.fun_args_values.get_mut().insert( + *func_ident, + BumpVec::from_iter_in(arena.get_or_default(), [closure_arg]), + ); queue_stack.get_mut().push(Action::LeaveScope(*func_ident)); queue_stack.get_mut().extend( take(&mut block.effects) @@ -1195,8 +1220,8 @@ async fn analyze_ecmascript_module_internal( } } Effect::Member { - obj, - prop, + mut obj, + mut prop, ast_path, span, } => { @@ -1206,10 +1231,11 @@ async fn analyze_ecmascript_module_internal( ); // Intentionally not awaited because `handle_member` reads this only when needed - let obj = analysis_state.link_value(*obj, ImportAttributes::empty_ref()); + let obj = + analysis_state.link_value(take(&mut *obj), ImportAttributes::empty_ref()); let prop = analysis_state - .link_value(*prop, ImportAttributes::empty_ref()) + .link_value(take(&mut *prop), ImportAttributes::empty_ref()) .await?; handle_member(&ast_path, obj, prop, span, &analysis_state, &mut analysis) @@ -1296,7 +1322,7 @@ async fn analyze_ecmascript_module_internal( } } Effect::TypeOf { - arg, + mut arg, ast_path, span, } => { @@ -1305,7 +1331,7 @@ async fn analyze_ecmascript_module_internal( "unexpected Effect::TypeOf in tracing mode" ); let arg = analysis_state - .link_value(*arg, ImportAttributes::empty_ref()) + .link_value(take(&mut *arg), ImportAttributes::empty_ref()) .await?; handle_typeof(&ast_path, arg, span, &analysis_state, &mut analysis).await?; } @@ -1469,12 +1495,12 @@ async fn compile_time_info_for_module_options( .cell()) } -async fn handle_call) + Send + Sync>( +async fn handle_call<'a, G: Fn(Vec>) + Send + Sync>( ast_path: &[AstParentKind], span: Span, - func: JsValue, - args: Vec, - state: &AnalysisState<'_>, + func: JsValue<'a>, + args: Vec>, + state: &AnalysisState<'a>, add_effects: &G, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, in_try: bool, @@ -1519,7 +1545,7 @@ async fn handle_call) + Send + Sync>( .get_or_try_init(|| async { unlinked_args .iter() - .cloned() + .map(|arg| arg.clone_in(state.arena.get_or_default())) .map(|arg| state.link_value(arg, ImportAttributes::empty_ref())) .try_join() .await @@ -1586,11 +1612,11 @@ async fn handle_call) + Send + Sync>( Ok(()) } -async fn handle_dynamic_import) + Send + Sync>( +async fn handle_dynamic_import<'a, G: Fn(Vec>) + Send + Sync>( ast_path: &[AstParentKind], span: Span, - args: Vec, - state: &AnalysisState<'_>, + args: Vec>, + state: &AnalysisState<'a>, add_effects: &G, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, in_try: bool, @@ -1636,7 +1662,7 @@ async fn handle_dynamic_import) + Send + Sync>( let linked_args = unlinked_args .iter() - .cloned() + .map(|arg| arg.clone_in(state.arena.get_or_default())) .map(|arg| state.link_value(arg, ImportAttributes::empty_ref())) .try_join() .await?; @@ -1661,7 +1687,7 @@ async fn handle_dynamic_import) + Send + Sync>( async fn handle_dynamic_import_with_linked_args( ast_path: &[AstParentKind], span: Span, - linked_args: &[JsValue], + linked_args: &[JsValue<'_>], handler: &Handler, origin: ResolvedVc>, source: ResolvedVc>, @@ -1744,8 +1770,8 @@ async fn handle_dynamic_import_with_linked_args( Ok(()) } -async fn handle_well_known_function_call<'a, F, Fut>( - func: WellKnownFunctionKind, +async fn handle_well_known_function_call<'a, 'l, F, Fut>( + func: WellKnownFunctionKind<'a>, new: bool, linked_args: &F, handler: &Handler, @@ -1758,16 +1784,17 @@ async fn handle_well_known_function_call<'a, F, Fut>( source: ResolvedVc>, ast_path: &[AstParentKind], in_try: bool, - state: &'a AnalysisState<'a>, + state: &AnalysisState<'a>, collect_affecting_sources: bool, tracing_only: bool, attributes: &ImportAttributes, ) -> Result<()> where + 'a: 'l, F: Fn() -> Fut, - Fut: Future>>, + Fut: Future>>>, { - fn explain_args(args: &[JsValue]) -> (String, String) { + fn explain_args(args: &[JsValue<'_>]) -> (String, String) { JsValue::explain_args(args, 10, 2) } @@ -1805,14 +1832,10 @@ where match func { WellKnownFunctionKind::URLConstructor => { let args = linked_args().await?; - if let [ - url, - JsValue::Member( - _, - box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), - box JsValue::Constant(super::analyzer::ConstantValue::Str(meta_prop)), - ), - ] = &args[..] + if let [url, JsValue::Member(_, member_obj, member_prop)] = &args[..] + && let JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta) = &**member_obj + && let JsValue::Constant(super::analyzer::ConstantValue::Str(meta_prop)) = + &**member_prop && meta_prop.as_str() == "url" { let pat = js_value_to_pattern(url); @@ -2347,10 +2370,18 @@ where let linked_func_call = state .link_value( JsValue::call_from_parts( - JsValue::WellKnownFunction(WellKnownFunctionKind::PathResolve(Box::new( - parent_path.path.as_str().into(), - ))), - args.clone(), + state.arena.get_or_default(), + JsValue::WellKnownFunction(WellKnownFunctionKind::PathResolve( + state + .arena + .get_or_default() + .alloc(parent_path.path.as_str().into()), + )), + BumpVec::from_iter_in( + state.arena.get_or_default(), + args.iter() + .map(|a| a.clone_in(state.arena.get_or_default())), + ), ), ImportAttributes::empty_ref(), ) @@ -2396,8 +2427,13 @@ where let linked_func_call = state .link_value( JsValue::call_from_parts( + state.arena.get_or_default(), JsValue::WellKnownFunction(WellKnownFunctionKind::PathJoin), - args.clone(), + BumpVec::from_iter_in( + state.arena.get_or_default(), + args.iter() + .map(|a| a.clone_in(state.arena.get_or_default())), + ), ), ImportAttributes::empty_ref(), ) @@ -2441,8 +2477,11 @@ where let mut show_dynamic_warning = false; let pat = js_value_to_pattern(&args[0]); if pat.is_match_ignore_dynamic("node") && args.len() >= 2 { - let first_arg = - JsValue::member(Box::new(args[1].clone()), Box::new(0_f64.into())); + let first_arg = JsValue::member( + state.arena.get_or_default(), + args[1].clone_in(state.arena.get_or_default()), + 0_f64.into(), + ); let first_arg = state .link_value(first_arg, ImportAttributes::empty_ref()) .await?; @@ -2603,7 +2642,10 @@ where let args = linked_args().await?; if args.len() == 1 { let first_arg = state - .link_value(args[0].clone(), ImportAttributes::empty_ref()) + .link_value( + args[0].clone_in(state.arena.get_or_default()), + ImportAttributes::empty_ref(), + ) .await?; if let Some(s) = first_arg.as_str() { // TODO this resolving should happen within Vc @@ -2642,7 +2684,10 @@ where let args = linked_args().await?; if args.len() == 1 { let first_arg = state - .link_value(args[0].clone(), ImportAttributes::empty_ref()) + .link_value( + args[0].clone_in(state.arena.get_or_default()), + ImportAttributes::empty_ref(), + ) .await?; if let Some(s) = first_arg.as_str() { analysis.add_reference( @@ -2693,13 +2738,14 @@ where } else { let linked_func_call = state .link_value( - JsValue::call_from_parts( + JsValue::call_from_iter( + state.arena.get_or_default(), JsValue::WellKnownFunction( WellKnownFunctionKind::PathJoin, ), - vec![ + [ JsValue::FreeVar(atom!("__dirname")), - pkg_or_dir.clone(), + pkg_or_dir.clone_in(state.arena.get_or_default()), ], ), ImportAttributes::empty_ref(), @@ -2764,9 +2810,10 @@ where } else { let linked_func_call = state .link_value( - JsValue::call_from_parts( + JsValue::call_from_iter( + state.arena.get_or_default(), JsValue::WellKnownFunction(WellKnownFunctionKind::PathJoin), - vec![ + [ JsValue::FreeVar(atom!("__dirname")), p.into(), atom!("intl").into(), @@ -2943,7 +2990,7 @@ where /// Extracts dependency strings from the first argument of module.hot.accept/decline. /// Returns None if the argument is not a string or array of strings (e.g., it's a function /// for self-accept). -fn extract_hot_dep_strings(arg: &JsValue) -> Option> { +fn extract_hot_dep_strings(arg: &JsValue<'_>) -> Option> { // Single string: module.hot.accept('./dep', cb) if let Some(s) = arg.as_str() { return Some(vec![s.into()]); @@ -2959,12 +3006,12 @@ fn extract_hot_dep_strings(arg: &JsValue) -> Option> { None } -async fn handle_member( +async fn handle_member<'a>( ast_path: &[AstParentKind], - link_obj: impl Future> + Send + Sync, - prop: JsValue, + link_obj: impl Future>> + Send + Sync, + prop: JsValue<'a>, span: Span, - state: &AnalysisState<'_>, + state: &AnalysisState<'a>, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, ) -> Result<()> { if let Some(prop) = prop.as_str() { @@ -2980,7 +3027,7 @@ async fn handle_member( if has_member { let obj = obj.as_ref().unwrap(); - if let Some((mut name, false)) = obj.get_definable_name(Some(state.var_graph)) { + if let Some((mut name, false)) = obj.get_definable_name(Some(&state.var_graph)) { name.0.push(DefinableNameSegmentRef::Name(prop)); if let Some(value) = state .compile_time_info_ref @@ -3005,14 +3052,14 @@ async fn handle_member( Ok(()) } -async fn handle_typeof( +async fn handle_typeof<'a>( ast_path: &[AstParentKind], - arg: JsValue, + arg: JsValue<'a>, span: Span, - state: &AnalysisState<'_>, + state: &AnalysisState<'a>, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, ) -> Result<()> { - if let Some((mut name, false)) = arg.get_definable_name(Some(state.var_graph)) { + if let Some((mut name, false)) = arg.get_definable_name(Some(&state.var_graph)) { name.0.push(DefinableNameSegmentRef::TypeOf); if let Some(value) = state .compile_time_info_ref @@ -3028,11 +3075,11 @@ async fn handle_typeof( Ok(()) } -async fn handle_free_var( +async fn handle_free_var<'a>( ast_path: &[AstParentKind], - var: JsValue, + var: JsValue<'a>, span: Span, - state: &AnalysisState<'_>, + state: &AnalysisState<'a>, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, ) -> Result<()> { if let Some((name, _)) = var.get_definable_name(None) @@ -3203,7 +3250,7 @@ async fn analyze_amd_define( handler: &Handler, span: Span, ast_path: &[AstParentKind], - args: &[JsValue], + args: &[JsValue<'_>], error_mode: ResolveErrorMode, ) -> Result<()> { match args { @@ -3313,7 +3360,7 @@ async fn analyze_amd_define_with_deps( span: Span, ast_path: &[AstParentKind], id: Option<&str>, - deps: &[JsValue], + deps: &[JsValue<'_>], error_mode: ResolveErrorMode, ) -> Result<()> { let mut requests = Vec::new(); @@ -3399,21 +3446,26 @@ fn require_resolve(path: FileSystemPath) -> String { format!("/ROOT/{}", path.path.as_str()) } -async fn early_value_visitor(mut v: JsValue) -> Result<(JsValue, bool)> { +async fn early_value_visitor<'a>( + _arena: &'a ThreadLocal, + mut v: JsValue<'a>, +) -> Result<(JsValue<'a>, bool)> { let modified = early_replace_builtin(&mut v); Ok((v, modified)) } -async fn value_visitor( +async fn value_visitor<'a>( + arena: &'a ThreadLocal, origin: Vc>, - v: JsValue, + v: JsValue<'a>, compile_time_info: Vc, compile_time_info_ref: &CompileTimeInfo, - var_graph: &VarGraph, + var_graph: &VarGraph<'a>, attributes: &ImportAttributes, allow_project_root_tracing: bool, -) -> Result<(JsValue, bool)> { +) -> Result<(JsValue<'a>, bool)> { let (mut v, modified) = value_visitor_inner( + arena, origin, v, compile_time_info, @@ -3423,24 +3475,28 @@ async fn value_visitor( allow_project_root_tracing, ) .await?; - v.normalize_shallow(); + v.normalize_shallow(arena.get_or_default()); Ok((v, modified)) } -async fn value_visitor_inner( +async fn value_visitor_inner<'a>( + arena: &'a ThreadLocal, origin: Vc>, - v: JsValue, + v: JsValue<'a>, compile_time_info: Vc, compile_time_info_ref: &CompileTimeInfo, - var_graph: &VarGraph, + var_graph: &VarGraph<'a>, attributes: &ImportAttributes, allow_project_root_tracing: bool, -) -> Result<(JsValue, bool)> { +) -> Result<(JsValue<'a>, bool)> { let ImportAttributes { ignore, .. } = *attributes; if let Some((name, _)) = v.get_definable_name(Some(var_graph)) && let Some(value) = compile_time_info_ref.defines.get(&name).await? { - return Ok(((&*value).try_into()?, true)); + return Ok(( + JsValue::from_compile_time_define_value_in(arena.get_or_default(), &value)?, + true, + )); } let value = match v { JsValue::Call(_, call) @@ -3450,7 +3506,7 @@ async fn value_visitor_inner( ) => { let (_, args) = call.into_parts(); - require_resolve_visitor(origin, args).await? + require_resolve_visitor(arena, origin, args).await? } JsValue::Call(_, ref call) if matches!( @@ -3469,7 +3525,7 @@ async fn value_visitor_inner( ) => { let (_, args) = call.into_parts(); - require_context_visitor(origin, args).await? + require_context_visitor(arena, origin, args).await? } JsValue::Call(_, ref call) if matches!( @@ -3494,13 +3550,9 @@ async fn value_visitor_inner( JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire) ) => { - if let [ - JsValue::Member( - _, - box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), - box JsValue::Constant(super::analyzer::ConstantValue::Str(prop)), - ), - ] = call.args() + if let [JsValue::Member(_, member_obj, member_prop)] = call.args() + && let JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta) = &**member_obj + && let JsValue::Constant(super::analyzer::ConstantValue::Str(prop)) = &**member_prop && prop.as_str() == "url" { // `createRequire(import.meta.url)` @@ -3522,12 +3574,10 @@ async fn value_visitor_inner( { if let [ JsValue::Constant(super::analyzer::ConstantValue::Str(url)), - JsValue::Member( - _, - box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta), - box JsValue::Constant(super::analyzer::ConstantValue::Str(prop)), - ), + JsValue::Member(_, member_obj, member_prop), ] = call.args() + && let JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta) = &**member_obj + && let JsValue::Constant(super::analyzer::ConstantValue::Str(prop)) = &**member_prop { if prop.as_str() == "url" { JsValue::Url(url.clone(), JsValueUrlKind::Relative) @@ -3606,8 +3656,8 @@ async fn value_visitor_inner( ), _ => { let (mut v, mut modified) = - replace_well_known(v, compile_time_info, allow_project_root_tracing).await?; - modified = replace_builtin(&mut v) || modified; + replace_well_known(arena, v, compile_time_info, allow_project_root_tracing).await?; + modified = replace_builtin(arena.get_or_default(), &mut v) || modified; modified = modified || v.make_nested_operations_unknown(); return Ok((v, modified)); } @@ -3615,10 +3665,11 @@ async fn value_visitor_inner( Ok((value, true)) } -async fn require_resolve_visitor( +async fn require_resolve_visitor<'a>( + arena: &'a ThreadLocal, origin: Vc>, - args: Vec, -) -> Result { + args: BumpVec<'a, JsValue<'a>>, +) -> Result> { Ok(if args.len() == 1 { let pat = js_value_to_pattern(&args[0]); let request = Request::parse(pat.clone()); @@ -3644,6 +3695,7 @@ async fn require_resolve_visitor( match values.len() { 0 => JsValue::unknown( JsValue::call_from_parts( + arena.get_or_default(), JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve), args, ), @@ -3651,11 +3703,12 @@ async fn require_resolve_visitor( rcstr!("unresolvable request"), ), 1 => values.pop().unwrap(), - _ => JsValue::alternatives(values), + _ => JsValue::alternatives(BumpVec::from_iter_in(arena.get_or_default(), values)), } } else { JsValue::unknown( JsValue::call_from_parts( + arena.get_or_default(), JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve), args, ), @@ -3665,15 +3718,17 @@ async fn require_resolve_visitor( }) } -async fn require_context_visitor( +async fn require_context_visitor<'a>( + arena: &'a ThreadLocal, origin: Vc>, - args: Vec, -) -> Result { + args: BumpVec<'a, JsValue<'a>>, +) -> Result> { let options = match parse_require_context(&args) { Ok(options) => options, Err(err) => { return Ok(JsValue::unknown( JsValue::call_from_parts( + arena.get_or_default(), JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext), args, ), @@ -3742,7 +3797,7 @@ pub static TURBOPACK_HELPER_WTF8: LazyLock = /// `(process.argv[0], ['-e', ...])`. This is useful for detecting if a node /// process is being spawned to interpret a string of JavaScript code, and does /// not require static analysis. -fn is_invoking_node_process_eval(args: &[JsValue]) -> bool { +fn is_invoking_node_process_eval(args: &[JsValue<'_>]) -> bool { if args.len() < 2 { return false; } @@ -3750,9 +3805,9 @@ fn is_invoking_node_process_eval(args: &[JsValue]) -> bool { if let JsValue::Member(_, obj, constant) = &args[0] { // Is the first argument to spawn `process.argv[]`? if let ( - &box JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessArgv), - &box JsValue::Constant(JsConstantValue::Num(ConstantNumber(num))), - ) = (obj, constant) + JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessArgv), + JsValue::Constant(JsConstantValue::Num(ConstantNumber(num))), + ) = (&**obj, &**constant) { // Is it specifically `process.argv[0]`? if num.is_zero() diff --git a/turbopack/crates/turbopack-ecmascript/src/utils.rs b/turbopack/crates/turbopack-ecmascript/src/utils.rs index f30d80912612..543fdf2ae8ca 100644 --- a/turbopack/crates/turbopack-ecmascript/src/utils.rs +++ b/turbopack/crates/turbopack-ecmascript/src/utils.rs @@ -29,7 +29,7 @@ pub fn unparen(expr: &Expr) -> &Expr { } /// Converts a js-value into a Pattern for matching resources. -pub fn js_value_to_pattern(value: &JsValue) -> Pattern { +pub fn js_value_to_pattern(value: &JsValue<'_>) -> Pattern { match value { JsValue::Constant(v) => Pattern::Constant(match v { ConstantValue::Str(str) => { @@ -198,7 +198,7 @@ pub enum AstPathRange { /// Converts a module value (ie an import) to a well known object, /// which we specifically handle. -pub fn module_value_to_well_known_object(module_value: &ModuleValue) -> Option { +pub fn module_value_to_well_known_object<'a>(module_value: &ModuleValue) -> Option> { Some(match module_value.module.as_bytes() { b"node:path" | b"path" => JsValue::WellKnownObject(WellKnownObjectKind::PathModule), b"node:fs/promises" | b"fs/promises" => { @@ -300,12 +300,13 @@ mod tests { use turbopack_core::resolve::pattern::Pattern; use crate::{ - analyzer::{ConstantString, ConstantValue, JsValue}, + analyzer::{BumpVec, ConstantString, ConstantValue, JsValue, ThreadLocal}, utils::js_value_to_pattern, }; #[test] fn test_path_normalization_in_pattern() { + let arena = ThreadLocal::new(); assert_eq!( Pattern::Constant(rcstr!("hello/world")), js_value_to_pattern(&JsValue::Constant(ConstantValue::Str( @@ -317,11 +318,14 @@ mod tests { Pattern::Constant(rcstr!("hello/world")), js_value_to_pattern(&JsValue::Concat( 1, - vec![ - rcstr!("hello").into(), - rcstr!("\\").into(), - rcstr!("world").into() - ] + BumpVec::from_iter_in( + arena.get_or_default(), + [ + rcstr!("hello").into(), + rcstr!("\\").into(), + rcstr!("world").into() + ] + ) )) ); }