diff --git a/Cargo.lock b/Cargo.lock index d1eb14e02fb1e..efa39cdc3b63e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,6 +1637,7 @@ dependencies = [ name = "oxc_semantic" version = "0.13.3" dependencies = [ + "bitflags 2.5.0", "indexmap", "insta", "itertools 0.13.0", diff --git a/crates/oxc_linter/src/rules/eslint/getter_return.rs b/crates/oxc_linter/src/rules/eslint/getter_return.rs index e15782d816b1c..5e1a610941f33 100644 --- a/crates/oxc_linter/src/rules/eslint/getter_return.rs +++ b/crates/oxc_linter/src/rules/eslint/getter_return.rs @@ -184,7 +184,7 @@ impl GetterReturn { &cfg.graph, node.cfg_id(), &|edge| match edge { - EdgeType::Normal => None, + EdgeType::Jump | EdgeType::Normal => None, // We don't need to handle backedges because we would have already visited // them on the forward pass | EdgeType::Backedge @@ -269,6 +269,7 @@ impl GetterReturn { } // Ignore irrelevant elements. | InstructionKind::Break(_) + | InstructionKind::Continue(_) | InstructionKind::Statement => {} } } diff --git a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs index e367d1443c14d..3c945b89074c0 100644 --- a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs +++ b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs @@ -103,7 +103,7 @@ impl Rule for NoThisBeforeSuper { &cfg.graph, node.cfg_id(), &|edge| match edge { - EdgeType::Normal => None, + EdgeType::Jump | EdgeType::Normal => None, EdgeType::Unreachable | EdgeType::Backedge | EdgeType::NewFunction => { Some(DefinitelyCallsThisBeforeSuper::No) } diff --git a/crates/oxc_linter/src/rules/react/require_render_return.rs b/crates/oxc_linter/src/rules/react/require_render_return.rs index 3439c43391e77..75dd39f568118 100644 --- a/crates/oxc_linter/src/rules/react/require_render_return.rs +++ b/crates/oxc_linter/src/rules/react/require_render_return.rs @@ -98,7 +98,7 @@ fn contains_return_statement<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> b node.cfg_id(), &|edge| match edge { // We only care about normal edges having a return statement. - EdgeType::Normal => None, + EdgeType::Jump | EdgeType::Normal => None, // For these two type, we flag it as not found. EdgeType::Unreachable | EdgeType::NewFunction | EdgeType::Backedge => { Some(FoundReturn::No) @@ -122,6 +122,7 @@ fn contains_return_statement<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> b } InstructionKind::Return(ReturnInstructionKind::ImplicitUndefined) | InstructionKind::Break(_) + | InstructionKind::Continue(_) | InstructionKind::Statement => {} } } diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs index 4f6dceed3296d..b9fc7c03e00d3 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -286,7 +286,7 @@ impl RulesOfHooks { // All nodes should be reachable from our hook, Otherwise we have a conditional/branching flow. petgraph::algo::dijkstra(graph, func_cfg_id, Some(node_cfg_id), |e| match e.weight() { EdgeType::NewFunction => 1, - EdgeType::Unreachable | EdgeType::Backedge | EdgeType::Normal => 0, + EdgeType::Jump | EdgeType::Unreachable | EdgeType::Backedge | EdgeType::Normal => 0, }) .into_iter() .filter(|(_, val)| *val == 0) @@ -305,7 +305,7 @@ impl RulesOfHooks { &cfg.graph, func_cfg_id, &|e| match e { - EdgeType::Normal => None, + EdgeType::Jump | EdgeType::Normal => None, EdgeType::Unreachable | EdgeType::Backedge | EdgeType::NewFunction => { Some(State::default()) } @@ -324,7 +324,9 @@ impl RulesOfHooks { InstructionKind::Unreachable | InstructionKind::Throw | InstructionKind::Return(_) => FoldWhile::Continue((acc.0, false)), - InstructionKind::Statement => FoldWhile::Continue(acc), + InstructionKind::Statement | InstructionKind::Continue(_) => { + FoldWhile::Continue(acc) + } }) .into_inner(); diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index 3e808e818f4c2..d7ab2edcd2652 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -32,6 +32,7 @@ rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } petgraph = { workspace = true } itertools = { workspace = true } +bitflags = { workspace = true } tsify = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true } diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 9d0ad9bf31071..211056a1b8b23 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -17,8 +17,7 @@ use crate::{ checker::{EarlyErrorJavaScript, EarlyErrorTypeScript}, class::ClassTableBuilder, control_flow::{ - ControlFlowGraphBuilder, EdgeType, Register, ReturnInstructionKind, - StatementControlFlowType, + ControlFlowGraphBuilder, CtxCursor, CtxFlags, EdgeType, Register, ReturnInstructionKind, }, diagnostics::redeclaration, jsdoc::JSDocBuilder, @@ -28,7 +27,7 @@ use crate::{ reference::{Reference, ReferenceFlag, ReferenceId}, scope::{ScopeFlags, ScopeId, ScopeTree}, symbol::{SymbolFlags, SymbolId, SymbolTable}, - BreakInstructionKind, Semantic, + Semantic, }; pub struct SemanticBuilder<'a> { @@ -68,7 +67,7 @@ pub struct SemanticBuilder<'a> { check_syntax_error: bool, - pub cfg: ControlFlowGraphBuilder, + pub cfg: ControlFlowGraphBuilder<'a>, pub class_table_builder: ClassTableBuilder, } @@ -469,23 +468,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { stmt.scope_id.set(Some(self.current_scope_id)); self.enter_node(kind); - /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); - /* cfg */ - self.visit_statements(&stmt.body); - /* cfg */ - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); - /* cfg */ - self.leave_node(kind); self.leave_scope(); } @@ -496,56 +480,14 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let node_id = self.current_node_id; - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); /* cfg */ if let Some(ref break_target) = stmt.label { self.visit_label_identifier(break_target); - - /* cfg */ - if let Some(label_found) = - self.cfg.label_to_ast_node_ix.iter().rev().find(|x| x.0 == break_target.name) - { - let (_, break_, _) = self.cfg.ast_node_to_break_continue.iter().rev().find(|x| x.0 == label_found.1) - .expect("expected a corresponding break/continue array for a found label owning ast node"); - self.cfg.basic_blocks_with_breaks[*break_].push(self.cfg.current_node_ix); - } else { - self.cfg - .basic_blocks_with_breaks - .last_mut() - .expect( - "expected there to be a stack of control flows that a break can belong to", - ) - .push(self.cfg.current_node_ix); - } - /* cfg */ - } - /* cfg */ - else { - self.cfg - .basic_blocks_with_breaks - .last_mut() - .expect("expected there to be a stack of control flows that a break can belong to") - .push(self.cfg.current_node_ix); } - self.cfg.append_unreachable(); - self.cfg.push_break( - if stmt.label.is_some() { - BreakInstructionKind::Labeled - } else { - BreakInstructionKind::Unlabeled - }, - node_id, - ); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); + /* cfg */ + self.cfg.append_break(node_id, stmt.label.as_ref().map(|it| it.name.as_str())); /* cfg */ self.leave_node(kind); @@ -556,67 +498,15 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); + let node_id = self.current_node_id; /* cfg */ if let Some(continue_target) = &stmt.label { self.visit_label_identifier(continue_target); - /* cfg */ - if let Some(label_found) = - self.cfg.label_to_ast_node_ix.iter().rev().find(|x| x.0 == continue_target.name) - { - let (_, _, continue_) = self.cfg.ast_node_to_break_continue.iter().rev().find(|x| x.0 == label_found.1) - .expect("expected a corresponding break/continue array for a found label owning ast node"); - if let Some(continue_) = continue_ { - self.cfg.basic_blocks_with_breaks[*continue_].push(self.cfg.current_node_ix); - } else { - self.cfg - .basic_blocks_with_breaks - .last_mut() - .expect( - "expected there to be a stack of control flows that a break can belong to", - ) - .push(self.cfg.current_node_ix); - } - } else { - self.cfg - .basic_blocks_with_breaks - .last_mut() - .expect( - "expected there to be a stack of control flows that a break can belong to", - ) - .push(self.cfg.current_node_ix); - } - /* cfg */ - } - /* cfg */ - else { - self.cfg - .basic_blocks_with_breaks - .last_mut() - .expect("expected there to be a stack of control flows that a break can belong to") - .push(self.cfg.current_node_ix); } - self.cfg.append_unreachable(); - /* cfg */ /* cfg */ - let current_node_ix = self.cfg.current_node_ix; - // todo: assert on this instead when continues which - // aren't in iterations are nonrecoverable errors - if let Some(continues) = self.cfg.basic_blocks_with_continues.last_mut() { - continues.push(current_node_ix); - } - self.cfg.append_unreachable(); - - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); + self.cfg.append_continue(node_id, stmt.label.as_ref().map(|it| it.name.as_str())); /* cfg */ self.leave_node(kind); @@ -642,8 +532,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_do_while_stmt_graph_ix = self.cfg.current_node_ix; let start_body_graph_ix = self.cfg.new_basic_block(); - let statement_state = - self.cfg.preserve_state(self.current_node_id, StatementControlFlowType::UsesContinue); + + self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ self.visit_statement(&stmt.body); @@ -669,16 +559,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // end of condition to after start of body self.cfg.add_edge(end_of_condition_graph_ix, start_body_graph_ix, EdgeType::Backedge); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - // all basic blocks are break here so we connect them to the - // basic block after the do-while statement - end_do_while_graph_ix, - // all basic blocks are continues here so we connect them to the - // basic block of the condition - Some(start_of_condition_graph_ix), - ); + self.cfg + .ctx(None) + .mark_break(end_do_while_graph_ix) + .mark_continue(start_of_condition_graph_ix) + .resolve_with_upper_label(); /* cfg */ self.leave_node(kind); @@ -688,23 +573,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let kind = AstKind::ExpressionStatement(self.alloc(stmt)); self.enter_node(kind); - /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); - /* cfg */ - self.visit_expression(&stmt.expression); - /* cfg */ - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); - /* cfg */ - self.leave_node(kind); } @@ -853,8 +723,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_body_graph_ix = self.cfg.new_basic_block(); - let statement_state = - self.cfg.preserve_state(self.current_node_id, StatementControlFlowType::UsesContinue); + + self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ self.visit_statement(&stmt.body); @@ -868,18 +738,13 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.cfg.add_edge(update_graph_ix, test_graph_ix, EdgeType::Backedge); self.cfg.add_edge(after_test_graph_ix, after_for_stmt, EdgeType::Normal); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - // all basic blocks are break here so we connect them to the - // basic block after the for statement - self.cfg.current_node_ix, - // all basic blocks are continues here so we connect them to the - // basic block of the condition - Some(test_graph_ix), - ); - + self.cfg + .ctx(None) + .mark_break(after_for_stmt) + .mark_continue(test_graph_ix) + .resolve_with_upper_label(); /* cfg */ + self.leave_node(kind); if is_lexical_declaration { self.leave_scope(); @@ -923,8 +788,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // this basic block is always empty since there's no update condition in a for-in loop. let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block(); let body_graph_ix = self.cfg.new_basic_block(); - let statement_state = - self.cfg.preserve_state(self.current_node_id, StatementControlFlowType::UsesContinue); + + self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ self.visit_statement(&stmt.body); @@ -953,16 +818,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // for when there are no more iterations left in the iterable self.cfg.add_edge(basic_block_with_backedge_graph_ix, after_for_graph_ix, EdgeType::Normal); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - // all basic blocks are break here so we connect them to the - // basic block after the for-in statement - self.cfg.current_node_ix, - // all basic blocks are continues here so we connect them to the - // basic block of the condition - Some(basic_block_with_backedge_graph_ix), - ); + self.cfg + .ctx(None) + .mark_break(after_for_graph_ix) + .mark_continue(basic_block_with_backedge_graph_ix) + .resolve_with_upper_label(); /* cfg */ self.leave_node(kind); @@ -993,8 +853,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // this basic block is always empty since there's no update condition in a for-of loop. let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block(); let body_graph_ix = self.cfg.new_basic_block(); - let statement_state = - self.cfg.preserve_state(self.current_node_id, StatementControlFlowType::UsesContinue); + + self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ self.visit_statement(&stmt.body); @@ -1023,16 +883,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // for when there are no more iterations left in the iterable self.cfg.add_edge(basic_block_with_backedge_graph_ix, after_for_graph_ix, EdgeType::Normal); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - // all basic blocks are break here so we connect them to the - // basic block after the for-of statement - self.cfg.current_node_ix, - // all basic blocks are continues here so we connect them to the - // basic block of the condition - Some(basic_block_with_backedge_graph_ix), - ); + self.cfg + .ctx(None) + .mark_break(after_for_graph_ix) + .mark_continue(basic_block_with_backedge_graph_ix) + .resolve_with_upper_label(); /* cfg */ self.leave_node(kind); @@ -1048,13 +903,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_expression(&stmt.test); /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); - let before_if_stmt_graph_ix = self.cfg.current_node_ix; - - // if statement basic block let before_consequent_stmt_graph_ix = self.cfg.new_basic_block(); /* cfg */ @@ -1099,8 +948,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } else { self.cfg.add_edge(before_if_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal); } - - self.cfg.restore_state(&statement_state, self.current_node_id, after_if_graph_ix, None); /* cfg */ self.leave_node(kind); @@ -1111,9 +958,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); + let label = &stmt.label.name; + let ctx = self.cfg.ctx(Some(label.as_str())).default().allow_break(); + if stmt.body.is_iteration_statement() { + ctx.allow_continue(); + } /* cfg */ self.visit_label_identifier(&stmt.label); @@ -1125,13 +974,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_statement(&stmt.body); /* cfg */ - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); + let after_body_graph_ix = self.cfg.current_node_ix; + let after_labeled_stmt_graph_ix = self.cfg.new_basic_block(); + self.cfg.add_edge(after_body_graph_ix, after_labeled_stmt_graph_ix, EdgeType::Normal); + self.cfg.ctx(Some(label.as_str())).mark_break(after_labeled_stmt_graph_ix).resolve(); /* cfg */ self.leave_node(kind); @@ -1143,9 +990,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let node_id = self.current_node_id; - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); /* cfg */ let ret_kind = if let Some(arg) = &stmt.argument { @@ -1161,13 +1005,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - append unreachable after return */ self.cfg.append_unreachable(); - - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); /* cfg */ self.leave_node(kind); @@ -1183,9 +1020,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let discriminant_graph_ix = self.cfg.current_node_ix; self.cfg.switch_case_conditions.push(vec![]); - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); + + self.cfg.ctx(None).default().allow_break(); let mut ends_of_switch_cases = vec![]; /* cfg */ @@ -1233,12 +1069,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.cfg.add_edge(*last, self.cfg.current_node_ix, EdgeType::Normal); } - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); + let current_node_ix = self.cfg.current_node_ix; + self.cfg.ctx(None).mark_break(current_node_ix).resolve(); /* cfg */ self.leave_scope(); @@ -1280,9 +1112,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let node_id = self.current_node_id; - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); /* cfg */ self.visit_expression(&stmt.argument); @@ -1290,12 +1119,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ self.cfg.push_throw(node_id); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); /* cfg */ self.leave_node(kind); @@ -1363,10 +1186,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { // block, both for finally_succ and finally_err. /* cfg */ - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); - // TODO: support unwinding finally/catch blocks that aren't in this function // even if something throws. let parent_after_throw_block_ix = self.cfg.after_throw_block; @@ -1489,13 +1308,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { after_try_statement_block_ix, EdgeType::Normal, ); - - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); /* cfg */ self.leave_node(kind); @@ -1537,8 +1349,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - body basic block */ let body_graph_ix = self.cfg.new_basic_block(); - let statement_state = - self.cfg.preserve_state(self.current_node_id, StatementControlFlowType::UsesContinue); + + self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ self.visit_statement(&stmt.body); @@ -1552,16 +1364,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.cfg.add_edge(after_body_graph_ix, condition_graph_ix, EdgeType::Backedge); self.cfg.add_edge(condition_graph_ix, after_while_graph_ix, EdgeType::Normal); - self.cfg.restore_state( - &statement_state, - self.current_node_id, - // all basic blocks are break here so we connect them to the - // basic block after the while statement - after_body_graph_ix, - // all basic blocks are continues here so we connect them to the - // basic block of the condition - Some(condition_graph_ix), - ); + self.cfg + .ctx(None) + .mark_break(after_while_graph_ix) + .mark_continue(condition_graph_ix) + .resolve_with_upper_label(); /* cfg */ self.leave_node(kind); } @@ -1572,9 +1379,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - condition basic block */ let before_with_stmt_graph_ix = self.cfg.current_node_ix; - let statement_state = self - .cfg - .preserve_state(self.current_node_id, StatementControlFlowType::DoesNotUseContinue); + let condition_graph_ix = self.cfg.new_basic_block(); /* cfg */ @@ -1593,13 +1398,6 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.cfg.add_edge(condition_graph_ix, body_graph_ix, EdgeType::Normal); self.cfg.add_edge(body_graph_ix, after_body_graph_ix, EdgeType::Normal); self.cfg.add_edge(condition_graph_ix, after_body_graph_ix, EdgeType::Normal); - - self.cfg.restore_state( - &statement_state, - self.current_node_id, - self.cfg.current_node_ix, - None, - ); /* cfg */ self.leave_node(kind); @@ -1621,6 +1419,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let before_function_graph_ix = self.cfg.current_node_ix; let function_graph_ix = self.cfg.new_basic_block_for_function(); + self.cfg.ctx(None).new_function(); /* cfg */ // We add a new basic block to the cfg before entering the node @@ -1641,6 +1440,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ self.cfg.restore_expression_state(preserved); + self.cfg.ctx(None).resolve_expect(CtxFlags::FUNCTION); let after_function_graph_ix = self.cfg.new_basic_block(); self.cfg.add_edge(before_function_graph_ix, after_function_graph_ix, EdgeType::Normal); /* cfg */ @@ -1728,6 +1528,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let preserved = self.cfg.preserve_expression_state(); let current_node_ix = self.cfg.current_node_ix; let function_graph_ix = self.cfg.new_basic_block_for_function(); + self.cfg.ctx(None).new_function(); /* cfg */ // We add a new basic block to the cfg before entering the node @@ -1747,6 +1548,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ self.cfg.restore_expression_state(preserved); + self.cfg.ctx(None).resolve_expect(CtxFlags::FUNCTION); self.cfg.current_node_ix = current_node_ix; /* cfg */ if let Some(parameters) = &expr.type_parameters { @@ -1814,8 +1616,9 @@ impl<'a> SemanticBuilder<'a> { match kind { AstKind::ReturnStatement(_) | AstKind::BreakStatement(_) + | AstKind::ContinueStatement(_) | AstKind::ThrowStatement(_) => { /* These types have their own `InstructionKind`. */ } - _ if kind.is_statement() => { + it if it.is_statement() => { self.cfg.enter_statement(self.current_node_id); } _ => { /* ignore the rest */ } diff --git a/crates/oxc_semantic/src/control_flow/builder/context.rs b/crates/oxc_semantic/src/control_flow/builder/context.rs new file mode 100644 index 0000000000000..a63d1b7817419 --- /dev/null +++ b/crates/oxc_semantic/src/control_flow/builder/context.rs @@ -0,0 +1,250 @@ +use crate::{BasicBlockId, EdgeType}; + +use super::ControlFlowGraphBuilder; + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct CtxFlags: u8 { + /// Anything above a `FUNCTION` is unreachable. + const FUNCTION = 1; + const BREAK = 1 << 1; + const CONTINUE = 1 << 2; + } +} + +#[derive(Debug)] +pub(super) struct Ctx<'a> { + flags: CtxFlags, + label: Option<&'a str>, + entries: Vec<(CtxFlags, BasicBlockId)>, + break_jmp: Option, + continue_jmp: Option, +} + +impl<'a> Ctx<'a> { + fn new(label: Option<&'a str>, flags: CtxFlags) -> Self { + Self { flags, label, entries: Vec::new(), break_jmp: None, continue_jmp: None } + } + + fn is(&self, label: &str) -> bool { + self.label.as_ref().is_some_and(|it| *it == label) + } + + fn r#break(&mut self, entry: BasicBlockId) { + self.entries.push((CtxFlags::BREAK, entry)); + } + + fn r#continue(&mut self, entry: BasicBlockId) { + self.entries.push((CtxFlags::CONTINUE, entry)); + } +} + +pub trait CtxCursor { + /// Marks the break jump position in the current context. + fn mark_break(self, jmp_pos: BasicBlockId) -> Self; + /// Marks the continue jump position in the current context. + fn mark_continue(self, jmp_pos: BasicBlockId) -> Self; + /// Creates a break entry in the current context. + fn r#break(self, bb: BasicBlockId) -> Self; + /// Creates a continue entry in the current context. + fn r#continue(self, bb: BasicBlockId) -> Self; +} + +pub struct QueryCtx<'a, 'c>(&'c mut ControlFlowGraphBuilder<'a>, /* label */ Option<&'a str>); + +impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> { + fn mark_break(self, jmp_pos: BasicBlockId) -> Self { + self.0.in_break_context(self.1, |ctx| { + debug_assert!(ctx.break_jmp.is_none()); + ctx.break_jmp = Some(jmp_pos); + }); + self + } + + fn mark_continue(self, jmp_pos: BasicBlockId) -> Self { + self.0.in_continue_context(self.1, |ctx| { + debug_assert!(ctx.continue_jmp.is_none()); + ctx.continue_jmp = Some(jmp_pos); + }); + self + } + + fn r#break(self, bb: BasicBlockId) -> Self { + self.0.in_break_context(self.1, |ctx| { + ctx.r#break(bb); + }); + self + } + + fn r#continue(self, bb: BasicBlockId) -> Self { + self.0.in_continue_context(self.1, |ctx| { + ctx.r#continue(bb); + }); + self + } +} + +impl<'a, 'c> QueryCtx<'a, 'c> { + /// Creates a new `Ctx` with the given `CtxFlags` and returns a `RefCtxCursor` to it. + #[inline] + #[allow(clippy::wrong_self_convention, clippy::new_ret_no_self)] + pub fn new(self, flags: CtxFlags) -> RefCtxCursor<'a, 'c> { + #![allow(unsafe_code)] + self.0.ctx_stack.push(Ctx::new(self.1, flags)); + // SAFETY: we just pushed this `Ctx` into the stack. + let ctx = unsafe { self.0.ctx_stack.last_mut().unwrap_unchecked() }; + RefCtxCursor(ctx) + } + + /// Creates a new `Ctx` with empty `CtxFlags` and returns a `RefCtxCursor` to it. + pub fn default(self) -> RefCtxCursor<'a, 'c> { + self.new(CtxFlags::empty()) + } + + /// Creates a new `Ctx` with `CtxFlags::FUNCTION` set as its flags and returns a `RefCtxCursor` to it. + pub fn new_function(self) -> RefCtxCursor<'a, 'c> { + self.new(CtxFlags::FUNCTION) + } + + /// Resolves the current context and adds the required edges to the graph. + pub fn resolve(mut self) { + let Some(ctx) = self.0.ctx_stack.pop() else { return }; + self.resolve_ctx(ctx); + } + + /// Resolves the current context only if the expectations are satisfied. + /// + /// # Panics if there is no ctx on the stack or `expectation` isn't satisfied. + pub fn resolve_expect(mut self, expectation: CtxFlags) { + let ctx = self.0.ctx_stack.pop().expect("expected a `ctx` on the stack for resolution"); + assert!(ctx.flags.difference(expectation).is_empty()); + self.resolve_ctx(ctx); + } + + /// Resolves the current context and would mark the possible upper label + /// continue jump point the same as the resolved context. + pub fn resolve_with_upper_label(mut self) { + let Some(ctx) = self.0.ctx_stack.pop() else { return }; + + let continue_jmp = ctx.continue_jmp; + + self.resolve_ctx(ctx); + + // mark the upper label continue jump point the same as ours, + if let Some(jmp) = continue_jmp { + if let Some(label_ctx) = self.0.immediate_labeled_ctx() { + label_ctx.mark_continue(jmp); + } + } + } + + /// Resolves the current context and adds the required edges to the graph. + fn resolve_ctx(&mut self, ctx: Ctx<'a>) { + // TODO: This match is here to prevent redundant iterations and/or conditions by handling them + // before starting the iteration, I don't like the current implementation so it would be + // nice if we find a better way of doing it. + match (ctx.break_jmp, ctx.continue_jmp) { + (Some(break_), Some(continue_)) => { + for entry in ctx.entries { + match entry.0 { + CtxFlags::BREAK => self.0.add_edge(entry.1, break_, EdgeType::Jump), + CtxFlags::CONTINUE => self.0.add_edge(entry.1, continue_, EdgeType::Jump), + _ => {} + } + } + } + (Some(jmp), None) => { + for entry in ctx.entries { + if matches!(entry.0, CtxFlags::BREAK) { + self.0.add_edge(entry.1, jmp, EdgeType::Jump); + } + } + } + (None, Some(jmp)) => { + for entry in ctx.entries { + if matches!(entry.0, CtxFlags::CONTINUE) { + self.0.add_edge(entry.1, jmp, EdgeType::Jump); + } + } + } + (None, None) => {} + } + } +} + +pub struct RefCtxCursor<'a, 'c>(&'c mut Ctx<'a>); + +impl<'a, 'c> RefCtxCursor<'a, 'c> { + /// Allow break entries in this context. + pub fn allow_break(self) -> Self { + self.0.flags.insert(CtxFlags::BREAK); + self + } + /// Allow continue entries in this context. + pub fn allow_continue(self) -> Self { + self.0.flags.insert(CtxFlags::CONTINUE); + self + } +} + +impl<'a, 'c> CtxCursor for RefCtxCursor<'a, 'c> { + fn mark_break(self, jmp_pos: BasicBlockId) -> Self { + debug_assert!(self.0.break_jmp.is_none()); + self.0.break_jmp = Some(jmp_pos); + self + } + + fn mark_continue(self, jmp_pos: BasicBlockId) -> Self { + debug_assert!(self.0.continue_jmp.is_none()); + self.0.continue_jmp = Some(jmp_pos); + self + } + + fn r#break(self, bb: BasicBlockId) -> Self { + self.0.r#break(bb); + self + } + + fn r#continue(self, bb: BasicBlockId) -> Self { + self.0.r#continue(bb); + self + } +} + +impl<'a> ControlFlowGraphBuilder<'a> { + /// Query a control flow context. + pub fn ctx<'c>(&'c mut self, label: Option<&'a str>) -> QueryCtx<'a, 'c> { + QueryCtx(self, label) + } + + /// Returns `None` if there is no immediate labeled context before this call. + fn immediate_labeled_ctx<'c>(&'c mut self) -> Option> { + self.ctx_stack.last_mut().filter(|it| it.label.is_some()).map(RefCtxCursor) + } + + fn in_break_context)>(&mut self, label: Option<&str>, f: F) { + self.in_context(label, CtxFlags::BREAK, f); + } + + fn in_continue_context)>(&mut self, label: Option<&str>, f: F) { + self.in_context(label, CtxFlags::CONTINUE, f); + } + + fn in_context)>(&mut self, label: Option<&str>, flag: CtxFlags, f: F) { + let ctx = if let Some(label) = label { + self.ctx_stack + .iter_mut() + .rev() + // anything up the function is unreachable + .take_while(|it| !it.flags.intersects(CtxFlags::FUNCTION)) + .filter(|it| it.flags.contains(flag)) + .find(|it| it.is(label)) + } else { + self.ctx_stack.iter_mut().rev().find(|it| it.flags.contains(flag)) + }; + + if let Some(ctx) = ctx { + f(ctx); + } + } +} diff --git a/crates/oxc_semantic/src/control_flow/builder.rs b/crates/oxc_semantic/src/control_flow/builder/mod.rs similarity index 62% rename from crates/oxc_semantic/src/control_flow/builder.rs rename to crates/oxc_semantic/src/control_flow/builder/mod.rs index 0d4c79ffa2743..1d5422a9abc86 100644 --- a/crates/oxc_semantic/src/control_flow/builder.rs +++ b/crates/oxc_semantic/src/control_flow/builder/mod.rs @@ -1,16 +1,21 @@ -use crate::{BreakInstructionKind, ReturnInstructionKind}; +mod context; + +use crate::ReturnInstructionKind; +use context::Ctx; + +pub use context::{CtxCursor, CtxFlags}; use super::{ AstNodeId, BasicBlock, BasicBlockId, CompactStr, ControlFlowGraph, EdgeType, Graph, - Instruction, InstructionKind, PreservedExpressionState, PreservedStatementState, Register, - StatementControlFlowType, + Instruction, InstructionKind, LabeledInstruction, PreservedExpressionState, Register, }; #[derive(Debug, Default)] -pub struct ControlFlowGraphBuilder { +pub struct ControlFlowGraphBuilder<'a> { pub graph: Graph, pub basic_blocks: Vec, pub current_node_ix: BasicBlockId, + ctx_stack: Vec>, // note: this should only land in the big box for all things that take arguments // ie: callexpression, arrayexpression, etc // todo: add assert that it is used every time? @@ -34,7 +39,7 @@ pub struct ControlFlowGraphBuilder { pub after_throw_block: Option, } -impl ControlFlowGraphBuilder { +impl<'a> ControlFlowGraphBuilder<'a> { pub fn build(self) -> ControlFlowGraph { ControlFlowGraph { graph: self.graph, basic_blocks: self.basic_blocks } } @@ -95,8 +100,32 @@ impl ControlFlowGraphBuilder { self.push_instruction(InstructionKind::Throw, Some(node)); } - pub fn push_break(&mut self, kind: BreakInstructionKind, node: AstNodeId) { + pub fn append_break(&mut self, node: AstNodeId, label: Option<&'a str>) { + let kind = match label { + Some(_) => LabeledInstruction::Labeled, + None => LabeledInstruction::Unlabeled, + }; + + let bb = self.current_node_ix; + self.push_instruction(InstructionKind::Break(kind), Some(node)); + self.append_unreachable(); + + self.ctx(label).r#break(bb); + } + + pub fn append_continue(&mut self, node: AstNodeId, label: Option<&'a str>) { + let kind = match label { + Some(_) => LabeledInstruction::Labeled, + None => LabeledInstruction::Unlabeled, + }; + + let bb = self.current_node_ix; + + self.push_instruction(InstructionKind::Continue(kind), Some(node)); + self.append_unreachable(); + + self.ctx(label).r#continue(bb); } pub fn append_unreachable(&mut self) { @@ -136,83 +165,6 @@ impl ControlFlowGraphBuilder { preserved_state.store_final_assignments_into_this_array; } - // note: could use type specialization rather than an enum - #[must_use] - #[allow(clippy::needless_pass_by_value)] - pub fn preserve_state( - &mut self, - id: AstNodeId, - control_flow_type: StatementControlFlowType, - ) -> PreservedStatementState { - let mut pss = PreservedStatementState { put_label: false }; - - match control_flow_type { - StatementControlFlowType::DoesNotUseContinue => { - self.basic_blocks_with_breaks.push(vec![]); - if let Some(next_label) = &self.next_label.take() { - self.label_to_ast_node_ix.push((next_label.clone(), id)); - pss.put_label = true; - self.ast_node_to_break_continue.push(( - id, - self.basic_blocks_with_breaks.len() - 1, - None, - )); - } - } - StatementControlFlowType::UsesContinue => { - self.basic_blocks_with_breaks.push(vec![]); - self.basic_blocks_with_continues.push(vec![]); - if let Some(next_label) = &self.next_label.take() { - self.label_to_ast_node_ix.push((next_label.clone(), id)); - pss.put_label = true; - self.ast_node_to_break_continue.push(( - id, - self.basic_blocks_with_breaks.len() - 1, - None, - )); - } - } - } - - pss - } - - /// # Panics - pub fn restore_state( - &mut self, - preserved_state: &PreservedStatementState, - id: AstNodeId, - break_jump_position: BasicBlockId, - continue_jump_position: Option, - ) { - let basic_blocks_with_breaks = self - .basic_blocks_with_breaks - .pop() - .expect("expected there to be a breaks array for this statement"); - - for break_ in basic_blocks_with_breaks { - // can this always be self.current_node_ix? - self.add_edge(break_, break_jump_position, EdgeType::Normal); - } - - if let Some(continue_jump_position) = continue_jump_position { - let basic_blocks_with_continues = self.basic_blocks_with_continues.pop().expect( - "expect there to be a basic block with continue directive for this statement", - ); - - for continue_ in basic_blocks_with_continues { - self.add_edge(continue_, continue_jump_position, EdgeType::Normal); - } - } - - if preserved_state.put_label { - let popped = self.label_to_ast_node_ix.pop(); - let popped_2 = self.ast_node_to_break_continue.pop(); - debug_assert_eq!(popped.unwrap().1, id); - debug_assert_eq!(popped_2.unwrap().0, id); - } - } - pub fn enter_statement(&mut self, stmt: AstNodeId) { self.push_statement(stmt); } diff --git a/crates/oxc_semantic/src/control_flow/dot.rs b/crates/oxc_semantic/src/control_flow/dot.rs index 148a08228560c..1642ce83222bd 100644 --- a/crates/oxc_semantic/src/control_flow/dot.rs +++ b/crates/oxc_semantic/src/control_flow/dot.rs @@ -1,10 +1,13 @@ -use oxc_ast::{ast::BreakStatement, AstKind}; +use oxc_ast::{ + ast::{BreakStatement, ContinueStatement}, + AstKind, +}; use oxc_syntax::node::AstNodeId; use petgraph::dot::{Config, Dot}; use crate::{ - AstNode, AstNodes, BasicBlock, BreakInstructionKind, ControlFlowGraph, EdgeType, Instruction, - InstructionKind, ReturnInstructionKind, + AstNode, AstNodes, BasicBlock, ControlFlowGraph, EdgeType, Instruction, InstructionKind, + LabeledInstruction, ReturnInstructionKind, }; pub trait DisplayDot { @@ -71,8 +74,10 @@ impl DisplayDot for Instruction { InstructionKind::Statement => "statement", InstructionKind::Unreachable => "unreachable", InstructionKind::Throw => "throw", - InstructionKind::Break(BreakInstructionKind::Labeled) => "break