123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- /*
- * Copyright (c) 2022, Leon Albrecht <leon.a@serenityos.com>.
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <AK/Bitmap.h>
- #include <AK/TypeCasts.h>
- #include <LibJS/Bytecode/Op.h>
- #include <LibJS/Bytecode/PassManager.h>
- namespace JS::Bytecode::Passes {
- static NonnullOwnPtr<BasicBlock> eliminate_loads(BasicBlock const& block, size_t number_of_registers)
- {
- auto array_ranges = Bitmap::create(number_of_registers, false).release_value_but_fixme_should_propagate_errors();
- for (auto it = InstructionStreamIterator(block.instruction_stream()); !it.at_end(); ++it) {
- if ((*it).type() == Instruction::Type::NewArray) {
- Op::NewArray const& array_instruction = static_cast<Op::NewArray const&>(*it);
- if (size_t element_count = array_instruction.element_count())
- array_ranges.set_range<true, false>(array_instruction.start().index(), element_count);
- }
- }
- auto new_block = BasicBlock::create(block.name(), block.size());
- HashMap<size_t, Register> identifier_table {};
- HashMap<u32, Register> register_rerouting_table {};
- for (auto it = InstructionStreamIterator(block.instruction_stream()); !it.at_end();) {
- using enum Instruction::Type;
- // Note: When creating a variable, we technically purge the cache of any
- // variables of the same name;
- // In practice, we always generate a coinciding SetVariable, which
- // does the same
- switch ((*it).type()) {
- case GetVariable: {
- auto const& get_variable = static_cast<Op::GetVariable const&>(*it);
- ++it;
- auto const& next_instruction = *it;
- if (auto reg = identifier_table.find(get_variable.identifier().value()); reg != identifier_table.end()) {
- // If we have already seen a variable, we can replace its GetVariable with a simple Load
- // knowing that it was already stored in a register
- new (new_block->next_slot()) Op::Load(reg->value);
- new_block->grow(sizeof(Op::Load));
- if (next_instruction.type() == Instruction::Type::Store) {
- // If the next instruction is a Store, that is not meant to
- // construct an array, we can simply elide that store and reroute
- // all further references to the stores destination to the cached
- // instance of variable.
- // FIXME: We might be able to elide the previous load in the non-array case,
- // because we do not yet reuse the accumulator
- auto const& store = static_cast<Op::Store const&>(next_instruction);
- if (array_ranges.get(store.dst().index())) {
- // re-emit the store
- new (new_block->next_slot()) Op::Store(store);
- new_block->grow(sizeof(Op::Store));
- } else {
- register_rerouting_table.set(store.dst().index(), reg->value);
- }
- ++it;
- }
- continue;
- }
- // Otherwise we need to emit the GetVariable
- new (new_block->next_slot()) Op::GetVariable(get_variable);
- new_block->grow(sizeof(Op::GetVariable));
- // And if the next instruction is a Store, we can cache it's destination
- if (next_instruction.type() == Instruction::Type::Store) {
- auto const& store = static_cast<Op::Store const&>(next_instruction);
- identifier_table.set(get_variable.identifier().value(), store.dst());
- new (new_block->next_slot()) Op::Store(store);
- new_block->grow(sizeof(Op::Store));
- ++it;
- }
- continue;
- }
- case SetVariable: {
- // When a variable is set we need to remove it from the cache, because
- // we don't have an accurate view on it anymore
- // FIXME: If the previous instruction was a `Load $reg`, we could
- // update the cache instead
- auto const& set_variable = static_cast<Op::SetVariable const&>(*it);
- identifier_table.remove(set_variable.identifier().value());
- break;
- }
- case DeleteVariable: {
- // When a variable is deleted we need to remove it from the cache, it does not
- // exist anymore, although a variable of the same name may exist in upper scopes
- auto const& set_variable = static_cast<Op::DeleteVariable const&>(*it);
- identifier_table.remove(set_variable.identifier().value());
- break;
- }
- case Store: {
- // If we store to a position that we have are rerouting from,
- // we need to remove it from the routeing table
- // FIXME: This may be redundant due to us assigning to registers only once
- auto const& store = static_cast<Op::Store const&>(*it);
- register_rerouting_table.remove(store.dst().index());
- break;
- }
- case DeleteById:
- case DeleteByValue:
- // These can trigger proxies, which call into user code
- // So these are treated like calls
- case GetByValue:
- case GetById:
- case PutByValue:
- case PutById:
- // Attribute accesses (`a.o` or `a[o]`) may result in calls to getters or setters
- // or may trigger proxies
- // So these are treated like calls
- case Call:
- // Calls, especially to local functions and eval, may poison visible and
- // cached variables, hence we need to clear the lookup cache after emitting them
- // FIXME: In strict mode and with better identifier metrics, we might be able
- // to safe some caching with a more fine-grained identifier table
- // FIXME: We might be able to save some lookups to objects like `this`
- // which should not change their pointer
- memcpy(new_block->next_slot(), &*it, (*it).length());
- for (auto route : register_rerouting_table)
- reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(Register { route.key }, route.value);
- new_block->grow((*it).length());
- identifier_table.clear_with_capacity();
- ++it;
- continue;
- case NewBigInt:
- // FIXME: This is the only non trivially copyable Instruction,
- // so we need to do some extra work here
- new (new_block->next_slot()) Op::NewBigInt(static_cast<Op::NewBigInt const&>(*it));
- new_block->grow(sizeof(Op::NewBigInt));
- ++it;
- continue;
- default:
- break;
- }
- memcpy(new_block->next_slot(), &*it, (*it).length());
- for (auto route : register_rerouting_table) {
- // rerouting from key to value
- reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(Register { route.key }, route.value);
- }
- // because we are replacing the current block, we need to replace references
- // to ourselves here
- reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(block, *new_block);
- new_block->grow((*it).length());
- ++it;
- }
- return new_block;
- }
- void EliminateLoads::perform(PassPipelineExecutable& executable)
- {
- started();
- // FIXME: If we walk the CFG instead of the block list, we might be able to
- // save some work between blocks
- for (auto it = executable.executable.basic_blocks.begin(); it != executable.executable.basic_blocks.end(); ++it) {
- auto const& old_block = *it;
- auto new_block = eliminate_loads(*old_block, executable.executable.number_of_registers);
- // We will replace the old block, with a new one, so we need to replace all references,
- // to the old one with the new one
- for (auto& block : executable.executable.basic_blocks) {
- InstructionStreamIterator it { block->instruction_stream() };
- while (!it.at_end()) {
- auto& instruction = *it;
- ++it;
- const_cast<Instruction&>(instruction).replace_references(*old_block, *new_block);
- }
- }
- executable.executable.basic_blocks[it.index()] = move(new_block);
- }
- finished();
- }
- }
|