#[cfg(debug_assertions)]
use oxc_ast_visit::Visit;
use oxc_ast_visit::{VisitJs, walk_js::walk_call_expression};
use rustc_hash::FxHashSet;
use oxc_allocator::{BitSet, GetAllocator};
use oxc_ast::ast::*;
use oxc_semantic::Scoping;
use oxc_syntax::scope::{ScopeFlags, ScopeId};
use crate::{
ReusableTraverseCtx, TraverseCtx, minifier_traverse::traverse_mut_with_ctx,
peephole::PeepholeOptimizations, symbol_liveness, traverse_context::as_direct_eval_call,
};
#[must_use]
pub struct PassOutcome {
pub(crate) needs_another_pass: bool,
}
fn refresh_direct_eval_flags(scoping: &mut Scoping, direct_eval_scopes: &FxHashSet<ScopeId>) {
if direct_eval_scopes.is_empty() && !scoping.root_scope_flags().contains_direct_eval() {
return;
}
for index in 0..scoping.scopes_len() {
scoping.scope_flags_mut(ScopeId::from_usize(index)).remove(ScopeFlags::DirectEval);
}
for &scope_id in direct_eval_scopes {
let mut ancestor = Some(scope_id);
while let Some(scope_id) = ancestor {
let flags = scoping.scope_flags_mut(scope_id);
if flags.contains_direct_eval() {
break;
}
flags.insert(ScopeFlags::DirectEval);
ancestor = scoping.scope_parent_id(scope_id);
}
}
}
#[cfg(debug_assertions)]
fn debug_assert_no_over_prune(program: &Program<'_>, removed_references: &BitSet<'_>) {
struct OverPruneCheck<'b, 'c> {
removed_references: &'b BitSet<'c>,
}
impl<'a> Visit<'a> for OverPruneCheck<'_, '_> {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
let Some(reference_id) = it.reference_id.get() else { return };
let idx = reference_id.index();
assert!(
!self.removed_references.contains(idx),
"incremental scoping over-prune: reference {idx} is marked removed but still \
appears in the live program",
);
}
}
OverPruneCheck { removed_references }.visit_program(program);
}
#[cfg(debug_assertions)]
pub fn debug_assert_no_under_prune(
program: &Program<'_>,
ctx: &TraverseCtx<'_>,
initial_references_len: usize,
) {
struct LiveRefCollector<'b, 'c> {
live: &'b mut BitSet<'c>,
}
impl<'a> Visit<'a> for LiveRefCollector<'_, '_> {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
if let Some(reference_id) = it.reference_id.get() {
let idx = reference_id.index();
if idx < self.live.capacity() {
self.live.set_bit(idx);
}
}
}
}
let mut live = BitSet::new_in(initial_references_len, ctx.allocator());
LiveRefCollector { live: &mut live }.visit_program(program);
for reference_ids in ctx.scoping().resolved_references() {
for reference_id in reference_ids {
let idx = reference_id.index();
assert!(
idx >= initial_references_len || live.has_bit(idx),
"incremental scoping under-prune: reference {idx} is still in a symbol's \
resolved-references list but its node is gone from the program — a drop site \
bypassed the `drop_*` / `replace_*` helpers, or the caller passed a `scoping` \
inconsistent with `program`",
);
}
}
}
#[cfg(debug_assertions)]
fn debug_assert_no_stale_direct_eval(program: &Program<'_>, scoping: &Scoping) {
struct DirectEvalFlagCheck<'s> {
scoping: &'s Scoping,
}
impl<'a> VisitJs<'a> for DirectEvalFlagCheck<'_> {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Some(ident) = as_direct_eval_call(it)
&& let Some(reference_id) = ident.reference_id.get()
{
let reference = self.scoping.get_reference(reference_id);
if reference.symbol_id().is_none() {
for scope_id in self.scoping.scope_ancestors(reference.scope_id()) {
assert!(
self.scoping.scope_flags(scope_id).contains_direct_eval(),
"stale direct-eval flags: scope {scope_id:?} is missing \
`ScopeFlags::DirectEval` for a live direct `eval(...)` call — a \
pass formed a new direct eval call without dropping one — see \
`PassChanges::direct_eval_dropped`",
);
}
}
}
walk_call_expression(self, it);
}
}
if !scoping.root_unresolved_references().contains_key("eval") {
return;
}
DirectEvalFlagCheck { scoping }.visit_program(program);
}
fn flush_pass_changes(program: &Program<'_>, ctx: &mut TraverseCtx<'_>) -> bool {
let had_removed_references = !ctx.state.pass_changes.removed_references.is_empty();
let liveness_inputs_changed = ctx.state.pass_changes.direct_eval_dropped
|| (had_removed_references && symbol_liveness::dead_references_affect_analysis(ctx));
if had_removed_references {
#[cfg(debug_assertions)]
debug_assert_no_over_prune(program, &ctx.state.pass_changes.removed_references);
ctx.scoping
.scoping_mut()
.retain_resolved_references_excluding(&ctx.state.pass_changes.removed_references);
}
if ctx.state.pass_changes.direct_eval_dropped {
let scoping = ctx.scoping();
let mut live = LiveDirectEvalCollector::new(scoping);
live.visit_program(program);
let scopes = live.scopes;
refresh_direct_eval_flags(ctx.scoping_mut(), &scopes);
}
#[cfg(debug_assertions)]
debug_assert_no_stale_direct_eval(program, ctx.scoping());
let refs_len = ctx.scoping().references_len();
if ctx.state.pass_changes.removed_references.capacity() == refs_len {
if had_removed_references {
ctx.state.pass_changes.removed_references.clear();
}
} else {
ctx.state.pass_changes.removed_references = BitSet::new_in(refs_len, ctx.allocator());
}
ctx.state.pass_changes.direct_eval_dropped = false;
liveness_inputs_changed
}
fn finish_pass<'a>(
program: &Program<'a>,
ctx: &mut TraverseCtx<'a>,
force_liveness_analysis: bool,
) -> bool {
let liveness_inputs_changed = flush_pass_changes(program, ctx);
symbol_liveness::analyze(program, ctx, force_liveness_analysis || liveness_inputs_changed)
}
pub fn finish_normalize_pass<'a>(program: &Program<'a>, ctx: &mut TraverseCtx<'a>) {
let _ = ctx.state.take_revisit_requested();
finish_pass(program, ctx, true);
debug_assert_pass_changes_clean(ctx);
}
pub fn run_peephole_pass<'a>(
program: &mut Program<'a>,
ctx: &mut ReusableTraverseCtx<'a>,
) -> PassOutcome {
debug_assert_pass_changes_clean(ctx.get_mut());
ctx.state_mut().symbols.reset_values();
traverse_mut_with_ctx(&mut PeepholeOptimizations, program, ctx);
let ctx = ctx.get_mut();
let revisit_requested = ctx.state.take_revisit_requested();
let newly_dead = finish_pass(program, ctx, false);
debug_assert!(
!newly_dead || revisit_requested,
"ordinary liveness progress must follow a recorded pass change"
);
debug_assert_pass_changes_clean(ctx);
PassOutcome { needs_another_pass: revisit_requested || newly_dead }
}
#[inline]
fn debug_assert_pass_changes_clean(ctx: &TraverseCtx<'_>) {
debug_assert!(ctx.state.pass_changes_are_clean());
}
struct LiveDirectEvalCollector<'s> {
scoping: &'s Scoping,
scopes: FxHashSet<ScopeId>,
}
impl<'s> LiveDirectEvalCollector<'s> {
fn new(scoping: &'s Scoping) -> Self {
Self { scoping, scopes: FxHashSet::default() }
}
}
impl<'a> VisitJs<'a> for LiveDirectEvalCollector<'_> {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Some(ident) = as_direct_eval_call(it)
&& let Some(reference_id) = ident.reference_id.get()
{
let scope_id = self.scoping.get_reference(reference_id).scope_id();
self.scopes.insert(scope_id);
}
walk_call_expression(self, it);
}
}