123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /*
- * Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <AK/TemporaryChange.h>
- #include <LibJS/Bytecode/PassManager.h>
- namespace JS::Bytecode::Passes {
- struct UnwindFrame {
- BasicBlock const* handler;
- BasicBlock const* finalizer;
- Vector<BasicBlock const*> finalizer_targets;
- };
- static HashTable<BasicBlock const*> seen_blocks;
- static Vector<UnwindFrame> unwind_frames;
- static BasicBlock const* next_handler_or_finalizer()
- {
- return unwind_frames.last().handler ?: unwind_frames.last().finalizer;
- }
- static void generate_cfg_for_block(BasicBlock const& current_block, PassPipelineExecutable& executable)
- {
- seen_blocks.set(¤t_block);
- auto enter_label = [&](Label const& label, BasicBlock const& entering_block) {
- executable.cfg->ensure(&entering_block).set(&label.block());
- executable.inverted_cfg->ensure(&label.block()).set(&entering_block);
- // The finalizers and handlers of an unwind context are handled separately
- if (!seen_blocks.contains(&label.block())
- && &label.block() != unwind_frames.last().handler
- && &label.block() != unwind_frames.last().finalizer)
- generate_cfg_for_block(label.block(), executable);
- };
- if (auto const* block = next_handler_or_finalizer())
- enter_label(Label { *block }, current_block);
- for (InstructionStreamIterator it { current_block.instruction_stream() }; !it.at_end(); ++it) {
- auto const& instruction = *it;
- if (instruction.type() == Instruction::Type::LeaveUnwindContext) {
- if (unwind_frames.last().finalizer && unwind_frames.last().finalizer != ¤t_block)
- dbgln("FIXME: Popping finalizer from the unwind context from outside the finalizer");
- unwind_frames.take_last();
- if (auto const* block = next_handler_or_finalizer())
- enter_label(Label { *block }, current_block);
- }
- if (!instruction.is_terminator())
- continue;
- using enum Instruction::Type;
- switch (instruction.type()) {
- case Jump: {
- auto true_target = *static_cast<Op::Jump const&>(instruction).true_target();
- enter_label(true_target, current_block);
- return;
- }
- case JumpIf:
- case JumpNullish:
- case JumpUndefined: {
- // FIXME: It would be nice if we could avoid this copy, if we know that the unwind context stays the same in both paths
- // Or with a COW capable Vector alternative
- // Note: We might partially unwind here, so we need to make a copy of
- // the current context to assure that the falsy code path has the same one
- {
- TemporaryChange saved_context { unwind_frames, unwind_frames };
- auto true_target = *static_cast<Op::Jump const&>(instruction).true_target();
- enter_label(true_target, current_block);
- }
- auto false_target = *static_cast<Op::Jump const&>(instruction).false_target();
- enter_label(false_target, current_block);
- return;
- }
- case Yield: {
- auto continuation = static_cast<Op::Yield const&>(instruction).continuation();
- if (continuation.has_value()) {
- executable.exported_blocks->set(&continuation->block());
- enter_label(*continuation, current_block);
- } else if (auto const* finalizer = unwind_frames.last().finalizer) {
- enter_label(Label { *finalizer }, current_block);
- unwind_frames.last().finalizer_targets.append(nullptr);
- }
- return;
- }
- case Await: {
- auto const& continuation = static_cast<Op::Await const&>(instruction).continuation();
- executable.exported_blocks->set(&continuation.block());
- enter_label(continuation, current_block);
- return;
- }
- case EnterUnwindContext: {
- auto entry_point = static_cast<Op::EnterUnwindContext const&>(instruction).entry_point();
- auto handler_target = static_cast<Op::EnterUnwindContext const&>(instruction).handler();
- auto finalizer_target = static_cast<Op::EnterUnwindContext const&>(instruction).finalizer();
- // We keep the frame alive here on the stack, to save some allocation size
- UnwindFrame frame {
- .handler = handler_target.has_value() ? &handler_target->block() : nullptr,
- .finalizer = finalizer_target.has_value() ? &finalizer_target->block() : nullptr,
- .finalizer_targets = {}
- };
- unwind_frames.append(frame);
- {
- // This will enter the handler and finalizer when needed.
- TemporaryChange saved_context { unwind_frames, unwind_frames };
- enter_label(entry_point, current_block);
- }
- frame.handler = nullptr;
- if (handler_target.has_value()) {
- // We manually generate the CFG, because we previously skiped it
- TemporaryChange saved_context { unwind_frames, unwind_frames };
- generate_cfg_for_block(handler_target->block(), executable);
- }
- if (finalizer_target.has_value()) {
- // We manually generate the CFG, because we previously halted before entering it
- generate_cfg_for_block(finalizer_target->block(), executable);
- // We previously halted execution when we would enter the finalizer,
- // So we now have to visit all possible targets
- // This mainly affects the ScheduleJump instruction
- for (auto const* block : frame.finalizer_targets) {
- if (block == nullptr) {
- // This signals a `return`, which we do not handle specially, so we skip
- continue;
- }
- if (!seen_blocks.contains(block))
- generate_cfg_for_block(*block, executable);
- }
- } else {
- unwind_frames.take_last();
- VERIFY(frame.finalizer_targets.is_empty());
- }
- return;
- }
- case ContinuePendingUnwind: {
- auto resume_target = static_cast<Op::ContinuePendingUnwind const&>(instruction).resume_target();
- enter_label(resume_target, current_block);
- // Note: We already mark these possible control flow changes further up, but when we get
- // get better error awareness, being explicit here will be required
- if (auto const* handler = unwind_frames.last().handler)
- enter_label(Label { *handler }, current_block);
- else if (auto const* finalizer = unwind_frames.last().finalizer)
- enter_label(Label { *finalizer }, current_block);
- return;
- }
- case Throw:
- // Note: We technically register that we enter the handler in the prelude,
- // but lets be correct and mark it again,
- // this will be useful once we have more info on which instruction can
- // actually fail
- if (auto const* handler = unwind_frames.last().handler) {
- enter_label(Label { *handler }, current_block);
- } else if (auto const* finalizer = unwind_frames.last().finalizer) {
- enter_label(Label { *finalizer }, current_block);
- // Note: This error might bubble through the finalizer to the next handler/finalizer,
- // This is currently marked in the general path
- }
- return;
- case Return:
- if (auto const* finalizer = unwind_frames.last().finalizer) {
- enter_label(Label { *finalizer }, current_block);
- unwind_frames.last().finalizer_targets.append(nullptr);
- }
- return;
- case ScheduleJump: {
- enter_label(Label { *unwind_frames.last().finalizer }, current_block);
- unwind_frames.last().finalizer_targets.append(
- &static_cast<Op::ScheduleJump const&>(instruction).target().block());
- return;
- }
- case End:
- return;
- default:
- dbgln("Unhandled terminator instruction: `{}`", instruction.to_byte_string(executable.executable));
- VERIFY_NOT_REACHED();
- };
- }
- // We have left the block, but not through a designated terminator,
- // so before we return, we need to check if we still need to go through a finalizer
- if (auto const* finalizer = unwind_frames.last().finalizer)
- enter_label(Label { *finalizer }, current_block);
- }
- void GenerateCFG::perform(PassPipelineExecutable& executable)
- {
- started();
- executable.cfg = HashMap<BasicBlock const*, HashTable<BasicBlock const*>> {};
- executable.inverted_cfg = HashMap<BasicBlock const*, HashTable<BasicBlock const*>> {};
- executable.exported_blocks = HashTable<BasicBlock const*> {};
- seen_blocks.clear();
- unwind_frames.clear();
- UnwindFrame top_level_frame = {};
- unwind_frames.append(top_level_frame);
- generate_cfg_for_block(*executable.executable.basic_blocks.first(), executable);
- finished();
- }
- }
|