| /* |
| * Copyright (C) 2019-2024 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "WasmBBQJIT.h" |
| #include "WasmBBQJIT64.h" |
| |
| #if ENABLE(WEBASSEMBLY_BBQJIT) |
| |
| #include "B3Common.h" |
| #include "B3ValueRep.h" |
| #include "BinarySwitch.h" |
| #include "BytecodeStructs.h" |
| #include "CCallHelpers.h" |
| #include "CPU.h" |
| #include "CompilerTimingScope.h" |
| #include "GPRInfo.h" |
| #include "JSCast.h" |
| #include "JSWebAssemblyArrayInlines.h" |
| #include "JSWebAssemblyException.h" |
| #include "JSWebAssemblyStruct.h" |
| #include "MacroAssembler.h" |
| #include "RegisterSet.h" |
| #include "WasmAddressType.h" |
| #include "WasmBBQDisassembler.h" |
| #include "WasmBaselineData.h" |
| #include "WasmCallProfile.h" |
| #include "WasmCallingConvention.h" |
| #include "WasmCompilationMode.h" |
| #include "WasmFormat.h" |
| #include "WasmFunctionParser.h" |
| #include "WasmIRGeneratorHelpers.h" |
| #include "WasmMemoryInformation.h" |
| #include "WasmMergedProfile.h" |
| #include "WasmModule.h" |
| #include "WasmModuleInformation.h" |
| #include "WasmOMGIRGenerator.h" |
| #include "WasmOperations.h" |
| #include "WasmOps.h" |
| #include "WasmThunks.h" |
| #include "WasmTypeDefinition.h" |
| #include <bit> |
| #include <cmath> |
| #include <wtf/Assertions.h> |
| #include <wtf/Compiler.h> |
| #include <wtf/HashFunctions.h> |
| #include <wtf/HashMap.h> |
| #include <wtf/MathExtras.h> |
| #include <wtf/PlatformRegisters.h> |
| #include <wtf/SmallSet.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/text/MakeString.h> |
| |
| namespace JSC { namespace Wasm { |
| |
| namespace BBQJITImpl { |
| |
| Location Location::none() |
| { |
| Location loc; |
| loc.m_kind = None; |
| return loc; |
| } |
| |
| Location Location::fromStack(int32_t stackOffset) |
| { |
| Location loc; |
| loc.m_kind = Stack; |
| loc.m_offset = stackOffset; |
| return loc; |
| } |
| |
| Location Location::fromStackArgument(int32_t stackOffset) |
| { |
| Location loc; |
| loc.m_kind = StackArgument; |
| loc.m_offset = stackOffset; |
| return loc; |
| } |
| |
| Location Location::fromGPR(GPRReg gpr) |
| { |
| Location loc; |
| loc.m_kind = Gpr; |
| loc.m_gpr = gpr; |
| return loc; |
| } |
| |
| Location Location::fromFPR(FPRReg fpr) |
| { |
| Location loc; |
| loc.m_kind = Fpr; |
| loc.m_fpr = fpr; |
| return loc; |
| } |
| |
| Location Location::fromGlobal(int32_t globalOffset) |
| { |
| Location loc; |
| loc.m_kind = Global; |
| loc.m_offset = globalOffset; |
| return loc; |
| } |
| |
| bool Location::isNone() const |
| { |
| return m_kind == None; |
| } |
| |
| bool Location::isGPR() const |
| { |
| return m_kind == Gpr; |
| } |
| |
| bool Location::isGPR2() const |
| { |
| return m_kind == Gpr2; |
| } |
| |
| bool Location::isFPR() const |
| { |
| return m_kind == Fpr; |
| } |
| |
| bool Location::isStack() const |
| { |
| return m_kind == Stack; |
| } |
| |
| bool Location::isStackArgument() const |
| { |
| return m_kind == StackArgument; |
| } |
| |
| bool Location::isGlobal() const |
| { |
| return m_kind == Global; |
| } |
| |
| bool Location::isMemory() const |
| { |
| return isStack() || isStackArgument() || isGlobal(); |
| } |
| |
| int32_t Location::asStackOffset() const |
| { |
| ASSERT(isStack()); |
| return m_offset; |
| } |
| |
| Address Location::asStackAddress() const |
| { |
| ASSERT(isStack()); |
| return Address(GPRInfo::callFrameRegister, asStackOffset()); |
| } |
| |
| int32_t Location::asGlobalOffset() const |
| { |
| ASSERT(isGlobal()); |
| return m_offset; |
| } |
| |
| Address Location::asGlobalAddress() const |
| { |
| ASSERT(isGlobal()); |
| return Address(GPRInfo::wasmContextInstancePointer, asGlobalOffset()); |
| } |
| |
| int32_t Location::asStackArgumentOffset() const |
| { |
| ASSERT(isStackArgument()); |
| return m_offset; |
| } |
| |
| Address Location::asStackArgumentAddress() const |
| { |
| ASSERT(isStackArgument()); |
| return Address(MacroAssembler::stackPointerRegister, asStackArgumentOffset()); |
| } |
| |
| Address Location::asAddress() const |
| { |
| switch (m_kind) { |
| case Stack: |
| return asStackAddress(); |
| case Global: |
| return asGlobalAddress(); |
| case StackArgument: |
| return asStackArgumentAddress(); |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| } |
| |
| GPRReg Location::asGPR() const |
| { |
| ASSERT(isGPR()); |
| return m_gpr; |
| } |
| |
| FPRReg Location::asFPR() const |
| { |
| ASSERT(isFPR()); |
| return m_fpr; |
| } |
| |
| GPRReg Location::asGPRlo() const |
| { |
| ASSERT(isGPR2()); |
| return m_gprlo; |
| } |
| |
| GPRReg Location::asGPRhi() const |
| { |
| ASSERT(isGPR2()); |
| return m_gprhi; |
| } |
| |
| void Location::dump(PrintStream& out) const |
| { |
| switch (m_kind) { |
| case None: |
| out.print("None"); |
| break; |
| case Gpr: |
| out.print("GPR(", MacroAssembler::gprName(m_gpr), ")"); |
| break; |
| case Fpr: |
| out.print("FPR(", MacroAssembler::fprName(m_fpr), ")"); |
| break; |
| case Stack: |
| out.print("Stack(", m_offset, ")"); |
| break; |
| case Global: |
| out.print("Global(", m_offset, ")"); |
| break; |
| case StackArgument: |
| out.print("StackArgument(", m_offset, ")"); |
| break; |
| case Gpr2: |
| out.print("GPR2(", m_gprhi, ",", m_gprlo, ")"); |
| break; |
| } |
| } |
| |
| bool Location::operator==(Location other) const |
| { |
| if (m_kind != other.m_kind) |
| return false; |
| switch (m_kind) { |
| case Gpr: |
| return m_gpr == other.m_gpr; |
| case Gpr2: |
| return m_gprlo == other.m_gprlo && m_gprhi == other.m_gprhi; |
| case Fpr: |
| return m_fpr == other.m_fpr; |
| case Stack: |
| case StackArgument: |
| case Global: |
| return m_offset == other.m_offset; |
| case None: |
| return true; |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| } |
| |
| Location::Kind Location::kind() const |
| { |
| return Kind(m_kind); |
| } |
| |
| bool BBQJIT::isValidValueTypeKind(TypeKind kind) |
| { |
| switch (kind) { |
| case TypeKind::I64: |
| case TypeKind::I32: |
| case TypeKind::F64: |
| case TypeKind::F32: |
| case TypeKind::V128: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| TypeKind BBQJIT::pointerType() { return is64Bit() ? TypeKind::I64 : TypeKind::I32; } |
| |
| bool BBQJIT::isFloatingPointType(TypeKind type) |
| { |
| return type == TypeKind::F32 || type == TypeKind::F64 || type == TypeKind::V128; |
| } |
| |
| TypeKind BBQJIT::toValueKind(TypeKind kind) |
| { |
| switch (kind) { |
| case TypeKind::I32: |
| case TypeKind::F32: |
| case TypeKind::I64: |
| case TypeKind::F64: |
| case TypeKind::V128: |
| return kind; |
| case TypeKind::Func: |
| case TypeKind::I31ref: |
| case TypeKind::Funcref: |
| case TypeKind::Exnref: |
| case TypeKind::Ref: |
| case TypeKind::RefNull: |
| case TypeKind::Rec: |
| case TypeKind::Sub: |
| case TypeKind::Subfinal: |
| case TypeKind::Struct: |
| case TypeKind::Structref: |
| case TypeKind::Externref: |
| case TypeKind::Array: |
| case TypeKind::Arrayref: |
| case TypeKind::Eqref: |
| case TypeKind::Anyref: |
| case TypeKind::Noexnref: |
| case TypeKind::Noneref: |
| case TypeKind::Nofuncref: |
| case TypeKind::Noexternref: |
| return TypeKind::I64; |
| case TypeKind::Void: |
| RELEASE_ASSERT_NOT_REACHED(); |
| return kind; |
| } |
| return kind; |
| } |
| |
| void Value::dump(PrintStream& out) const |
| { |
| switch (m_kind) { |
| case Const: |
| out.print("Const("); |
| if (m_type == TypeKind::I32) |
| out.print(m_i32); |
| else if (m_type == TypeKind::I64) |
| out.print(m_i64); |
| else if (m_type == TypeKind::F32) |
| out.print(m_f32); |
| else if (m_type == TypeKind::F64) |
| out.print(m_f64); |
| out.print(")"); |
| break; |
| case Local: |
| out.print("Local(", m_index, ")"); |
| break; |
| case Temp: |
| out.print("Temp(", m_index, ")"); |
| break; |
| case None: |
| out.print("None"); |
| break; |
| case Pinned: |
| out.print(m_pinned); |
| } |
| } |
| |
| RegisterBinding RegisterBinding::fromValue(Value value) |
| { |
| ASSERT(value.isLocal() || value.isTemp()); |
| RegisterBinding binding; |
| binding.m_type = value.type(); |
| binding.m_kind = value.isLocal() ? Local : Temp; |
| binding.m_index = value.isLocal() ? value.asLocal() : value.asTemp(); |
| return binding; |
| } |
| |
| RegisterBinding RegisterBinding::scratch() |
| { |
| RegisterBinding binding; |
| binding.m_kind = Scratch; |
| return binding; |
| } |
| |
| Value RegisterBinding::toValue() const |
| { |
| switch (m_kind) { |
| case None: |
| case Scratch: |
| return Value::none(); |
| case Local: |
| return Value::fromLocal(m_type, m_index); |
| case Temp: |
| return Value::fromTemp(m_type, m_index); |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| } |
| |
| bool RegisterBinding::operator==(RegisterBinding other) const |
| { |
| if (m_kind != other.m_kind) |
| return false; |
| |
| if (m_kind == None || m_kind == Scratch) |
| return true; |
| |
| return m_index == other.m_index && m_type == other.m_type; |
| } |
| |
| void RegisterBinding::dump(PrintStream& out) const |
| { |
| switch (m_kind) { |
| case None: |
| out.print("None"); |
| break; |
| case Scratch: |
| out.print("Scratch"); |
| break; |
| case Local: |
| out.print("Local(", m_index, ")"); |
| break; |
| case Temp: |
| out.print("Temp(", m_index, ")"); |
| break; |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| } |
| |
| unsigned RegisterBinding::hash() const |
| { |
| return pairIntHash(static_cast<unsigned>(m_kind), m_index); |
| } |
| |
| ControlData::ControlData(BBQJIT& generator, BlockType blockType, BlockSignature&& signature, LocalOrTempIndex enclosedHeight, RegisterSet liveScratchGPRs = { }, RegisterSet liveScratchFPRs = { }) |
| : m_signature(WTF::move(signature)) |
| , m_blockType(blockType) |
| , m_enclosedHeight(enclosedHeight) |
| { |
| if (blockType == BlockType::TopLevel) { |
| // For TopLevel, use the function's calling convention |
| CallInformation wasmCallInfo = wasmCallingConvention().callInformationFor(generator.m_functionSignature.get(), CallRole::Callee); |
| for (unsigned i = 0; i < m_signature.argumentCount(); ++i) |
| m_argumentLocations.append(Location::fromArgumentLocation(wasmCallInfo.params[i], m_signature.argumentType(i).kind)); |
| for (unsigned i = 0; i < m_signature.returnCount(); ++i) |
| m_resultLocations.append(Location::fromArgumentLocation(wasmCallInfo.results[i], m_signature.returnType(i).kind)); |
| return; |
| } |
| |
| if (!isAnyCatch(*this)) { |
| auto gprSetCopy = generator.validGPRs(); |
| auto fprSetCopy = generator.validFPRs(); |
| liveScratchGPRs.forEach([&] (auto r) { gprSetCopy.remove(r); }); |
| liveScratchFPRs.forEach([&] (auto r) { fprSetCopy.remove(r); }); |
| |
| for (unsigned i = 0; i < m_signature.argumentCount(); ++i) |
| m_argumentLocations.append(allocateArgumentOrResult(generator, m_signature.argumentType(i).kind, i, gprSetCopy, fprSetCopy)); |
| } |
| |
| auto gprSetCopy = generator.validGPRs(); |
| auto fprSetCopy = generator.validFPRs(); |
| for (unsigned i = 0; i < m_signature.returnCount(); ++i) |
| m_resultLocations.append(allocateArgumentOrResult(generator, m_signature.returnType(i).kind, i, gprSetCopy, fprSetCopy)); |
| } |
| |
| // This function is intentionally not using implicitSlots since arguments and results should not include implicit slot. |
| |
| void ControlData::convertIfToBlock() |
| { |
| ASSERT(m_blockType == BlockType::If); |
| m_blockType = BlockType::Block; |
| } |
| |
| void ControlData::convertLoopToBlock() |
| { |
| ASSERT(m_blockType == BlockType::Loop); |
| m_blockType = BlockType::Block; |
| } |
| |
| void ControlData::addBranch(Jump jump) |
| { |
| m_branchList.append(jump); |
| } |
| |
| void ControlData::addLabel(Box<CCallHelpers::Label>&& label) |
| { |
| m_labels.append(WTF::move(label)); |
| } |
| |
| void ControlData::delegateJumpsTo(ControlData& delegateTarget) |
| { |
| delegateTarget.m_branchList.append(std::exchange(m_branchList, { })); |
| delegateTarget.m_labels.appendVector(std::exchange(m_labels, { })); |
| } |
| |
| void ControlData::linkJumps(MacroAssembler::AbstractMacroAssemblerType* masm) |
| { |
| m_branchList.link(masm); |
| fillLabels(masm->label()); |
| } |
| |
| void ControlData::linkJumpsTo(MacroAssembler::Label label, MacroAssembler::AbstractMacroAssemblerType* masm) |
| { |
| m_branchList.linkTo(label, masm); |
| fillLabels(label); |
| } |
| |
| void ControlData::linkIfBranch(MacroAssembler::AbstractMacroAssemblerType* masm) |
| { |
| ASSERT(m_blockType == BlockType::If); |
| if (m_ifBranch.isSet()) |
| m_ifBranch.link(masm); |
| } |
| |
| void ControlData::dump(PrintStream& out) const |
| { |
| UNUSED_PARAM(out); |
| } |
| |
| LocalOrTempIndex ControlData::enclosedHeight() const |
| { |
| return m_enclosedHeight; |
| } |
| |
| unsigned ControlData::implicitSlots() const |
| { |
| return isAnyCatch(*this) ? 1 : 0; |
| } |
| |
| const Vector<Location, 2>& ControlData::targetLocations() const |
| { |
| return blockType() == BlockType::Loop |
| ? argumentLocations() |
| : resultLocations(); |
| } |
| |
| const Vector<Location, 2>& ControlData::argumentLocations() const |
| { |
| return m_argumentLocations; |
| } |
| |
| const Vector<Location, 2>& ControlData::resultLocations() const |
| { |
| return m_resultLocations; |
| } |
| |
| BlockType ControlData::blockType() const { return m_blockType; } |
| const BlockSignature& ControlData::signature() const { return m_signature; } |
| |
| FunctionArgCount ControlData::branchTargetArity() const |
| { |
| if (blockType() == BlockType::Loop) |
| return m_signature.argumentCount(); |
| return m_signature.returnCount(); |
| } |
| |
| Type ControlData::branchTargetType(unsigned i) const |
| { |
| ASSERT(i < branchTargetArity()); |
| if (m_blockType == BlockType::Loop) |
| return m_signature.argumentType(i); |
| return m_signature.returnType(i); |
| } |
| |
| Type ControlData::argumentType(unsigned i) const |
| { |
| ASSERT(i < m_signature.argumentCount()); |
| return m_signature.argumentType(i); |
| } |
| |
| CatchKind ControlData::catchKind() const |
| { |
| ASSERT(m_blockType == BlockType::Catch); |
| return m_catchKind; |
| } |
| |
| void ControlData::setCatchKind(CatchKind catchKind) |
| { |
| ASSERT(m_blockType == BlockType::Catch); |
| m_catchKind = catchKind; |
| } |
| |
| unsigned ControlData::tryStart() const |
| { |
| return m_tryStart; |
| } |
| |
| unsigned ControlData::tryEnd() const |
| { |
| return m_tryEnd; |
| } |
| |
| unsigned ControlData::tryCatchDepth() const |
| { |
| return m_tryCatchDepth; |
| } |
| |
| void ControlData::setTryInfo(unsigned tryStart, unsigned tryEnd, unsigned tryCatchDepth) |
| { |
| m_tryStart = tryStart; |
| m_tryEnd = tryEnd; |
| m_tryCatchDepth = tryCatchDepth; |
| } |
| |
| void ControlData::setTryTableTargets(TargetList &&targets) |
| { |
| m_tryTableTargets = WTF::move(targets); |
| } |
| |
| void ControlData::setIfBranch(MacroAssembler::Jump branch) |
| { |
| ASSERT(m_blockType == BlockType::If); |
| m_ifBranch = branch; |
| } |
| |
| void ControlData::setLoopLabel(MacroAssembler::Label label) |
| { |
| ASSERT(m_blockType == BlockType::Loop); |
| m_loopLabel = label; |
| } |
| |
| const MacroAssembler::Label& ControlData::loopLabel() const |
| { |
| return m_loopLabel; |
| } |
| |
| void ControlData::touch(LocalOrTempIndex local) |
| { |
| m_touchedLocals.add(local); |
| } |
| |
| void ControlData::fillLabels(CCallHelpers::Label label) |
| { |
| for (auto& box : m_labels) |
| *box = label; |
| } |
| |
| BBQJIT::BBQJIT(CompilationContext& compilationContext, const RTT& signature, Module& module, CalleeGroup& calleeGroup, IPIntCallee& profiledCallee, BBQCallee& callee, const FunctionData& function, FunctionCodeIndex functionIndex, const ModuleInformation& info, Vector<UnlinkedWasmToWasmCall>& unlinkedWasmToWasmCalls, MemoryMode mode, InternalFunction* compilation) |
| : m_context(compilationContext) |
| , m_jit(*compilationContext.wasmEntrypointJIT) |
| , m_module(module) |
| , m_calleeGroup(calleeGroup) |
| , m_profiledCallee(profiledCallee) |
| , m_callee(callee) |
| , m_function(function) |
| , m_functionSignature(signature) |
| , m_functionIndex(functionIndex) |
| , m_info(info) |
| , m_mode(mode) |
| , m_unlinkedWasmToWasmCalls(unlinkedWasmToWasmCalls) |
| , m_directCallees(m_info.internalFunctionCount()) |
| , m_lastUseTimestamp(0) |
| , m_compilation(compilation) |
| , m_pcToCodeOriginMapBuilder(Options::useSamplingProfiler()) |
| , m_profile(module.createMergedProfile(profiledCallee)) |
| { |
| RegisterSet gprSetBuilder = RegisterSet::allGPRs(); |
| gprSetBuilder.exclude(RegisterSet::specialRegisters()); |
| gprSetBuilder.exclude(RegisterSet::macroClobberedGPRs()); |
| gprSetBuilder.exclude(RegisterSet::wasmPinnedRegisters()); |
| gprSetBuilder.exclude(RegisterSet::bbqCalleeSaveRegisters()); |
| // FIXME: handle callee-saved registers better. |
| gprSetBuilder.exclude(RegisterSet::vmCalleeSaveRegisters()); |
| |
| RegisterSet fprSetBuilder = RegisterSet::allFPRs(); |
| RegisterSet::macroClobberedFPRs().forEach([&](Reg reg) { |
| fprSetBuilder.remove(reg); |
| }); |
| #if USE(JSVALUE32_64) && CPU(ARM_NEON) |
| // Remove NEON fprs |
| for (auto reg = ARMRegisters::d16; reg <= ARMRegisters::d31; reg = MacroAssembler::nextFPRegister(reg)) |
| fprSetBuilder.remove(reg); |
| #endif |
| // TODO: handle callee-saved registers better. |
| RegisterSet::vmCalleeSaveRegisters().forEach([&](Reg reg) { |
| fprSetBuilder.remove(reg); |
| }); |
| |
| RegisterSet callerSaveGprs = gprSetBuilder; |
| RegisterSet callerSaveFprs = fprSetBuilder; |
| |
| gprSetBuilder.remove(wasmScratchGPR); |
| #if USE(JSVALUE32_64) |
| gprSetBuilder.remove(wasmScratchGPR2); |
| #endif |
| fprSetBuilder.remove(wasmScratchFPR); |
| |
| ASCIILiteral logPrefix = Options::verboseBBQJITAllocation() ? "BBQ"_s : ASCIILiteral(); |
| m_gprAllocator.initialize(gprSetBuilder, logPrefix); |
| m_fprAllocator.initialize(fprSetBuilder, logPrefix); |
| m_callerSaveGPRs = callerSaveGprs; |
| m_callerSaveFPRs = callerSaveFprs; |
| m_callerSaves = callerSaveGprs.merge(callerSaveFprs); |
| |
| if (shouldDumpDisassemblyFor(CompilationMode::BBQMode)) [[unlikely]] { |
| m_disassembler = makeUnique<BBQDisassembler>(); |
| m_disassembler->setStartOfCode(m_jit.label()); |
| } |
| |
| CallInformation callInfo = wasmCallingConvention().callInformationFor(signature, CallRole::Callee); |
| |
| // Allocate callee save register spaces. |
| for (size_t i = 0, size = RegisterAtOffsetList::bbqCalleeSaveRegisters().registerCount(); i < size; ++i) |
| allocateStack(Value::fromPointer(nullptr)); |
| |
| ASSERT(callInfo.params.size() == m_functionSignature->argumentCount()); |
| for (unsigned i = 0; i < m_functionSignature->argumentCount(); i ++) { |
| const Type& type = m_functionSignature->argumentType(i); |
| m_localSlots.append(allocateStack(Value::fromLocal(type.kind, i))); |
| m_locals.append(Location::none()); |
| m_localTypes.append(type.kind); |
| |
| Value parameter = Value::fromLocal(type.kind, i); |
| bind(parameter, Location::fromArgumentLocation(callInfo.params[i], type.kind)); |
| m_arguments.append(i); |
| } |
| m_localAndCalleeSaveStorage = m_frameSizeForValidation; // All stack slots allocated so far are locals. |
| |
| unsigned ipintFrameBytes = m_profiledCallee.maxFrameSizeInV128() * sizeof(v128_t); |
| unsigned bbqCalleeSaveBytes = RegisterAtOffsetList::bbqCalleeSaveRegisters().registerCount() * sizeof(UCPURegister); |
| unsigned calleeStackBytes = std::max<unsigned>(m_profiledCallee.maxCalleeStackSize(), WTF::roundUpToMultipleOf<stackAlignmentBytes()>(WasmCallingConvention::headerSizeInBytes)); |
| unsigned scratchSpillBytes = (gprSetBuilder.numberOfSetRegisters() + fprSetBuilder.numberOfSetRegisters()) * tempSlotSize; |
| m_frameSize = alignedFrameSize(bbqCalleeSaveBytes + ipintFrameBytes + calleeStackBytes + scratchSpillBytes); |
| } |
| |
| bool BBQJIT::canTierUpToOMG() const |
| { |
| if (!Options::useOMGJIT()) |
| return false; |
| |
| if (!Options::useBBQTierUpChecks()) |
| return false; |
| |
| if (m_function.data.size() > Options::maximumOMGCandidateCost()) { |
| dataLogLnIf(Options::verboseOSR(), m_callee, ": Too large to tier-up to OMG: size = ", m_function.data.size()); |
| return false; |
| } |
| return true; |
| } |
| |
| void BBQJIT::emitIncrementCallProfileCount(unsigned callProfileIndex) |
| { |
| m_jit.add32(TrustedImm32(1), CCallHelpers::Address(GPRInfo::jitDataRegister, safeCast<int32_t>(BaselineData::offsetOfData() + sizeof(CallProfile) * callProfileIndex + CallProfile::offsetOfCount()))); |
| } |
| |
| void BBQJIT::setParser(FunctionParser<BBQJIT>* parser) |
| { |
| m_parser = parser; |
| } |
| |
| bool BBQJIT::addArguments(const RTT& signature) |
| { |
| RELEASE_ASSERT(m_arguments.size() == signature.argumentCount()); // We handle arguments in the prologue |
| return true; |
| } |
| |
| Value BBQJIT::addConstant(Type type, uint64_t value) |
| { |
| Value result; |
| switch (type.kind) { |
| case TypeKind::I32: |
| result = Value::fromI32(value); |
| LOG_INSTRUCTION("I32Const", RESULT(result)); |
| break; |
| case TypeKind::I64: |
| result = Value::fromI64(value); |
| LOG_INSTRUCTION("I64Const", RESULT(result)); |
| break; |
| case TypeKind::F32: { |
| int32_t tmp = static_cast<int32_t>(value); |
| result = Value::fromF32(*reinterpret_cast<float*>(&tmp)); |
| LOG_INSTRUCTION("F32Const", RESULT(result)); |
| break; |
| } |
| case TypeKind::F64: |
| result = Value::fromF64(*reinterpret_cast<double*>(&value)); |
| LOG_INSTRUCTION("F64Const", RESULT(result)); |
| break; |
| case TypeKind::Ref: |
| case TypeKind::RefNull: |
| case TypeKind::Structref: |
| case TypeKind::Arrayref: |
| case TypeKind::Funcref: |
| case TypeKind::Exnref: |
| case TypeKind::Externref: |
| case TypeKind::Eqref: |
| case TypeKind::Anyref: |
| case TypeKind::Noexnref: |
| case TypeKind::Noneref: |
| case TypeKind::Nofuncref: |
| case TypeKind::Noexternref: |
| result = Value::fromRef(type.kind, static_cast<EncodedJSValue>(value)); |
| LOG_INSTRUCTION("RefConst", makeString(type.kind), RESULT(result)); |
| break; |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Unimplemented constant type.\n"); |
| return Value::none(); |
| } |
| |
| if (Options::disableBBQConsts()) [[unlikely]] { |
| Value stackResult = topValue(type.kind); |
| emitStoreConst(result, canonicalSlot(stackResult)); |
| result = stackResult; |
| } |
| |
| return result; |
| } |
| |
| PartialResult BBQJIT::addDrop(Value value) |
| { |
| LOG_INSTRUCTION("Drop", value); |
| consume(value); |
| return { }; |
| } |
| |
| PartialResult BBQJIT::addLocal(Type type, uint32_t numberOfLocals) |
| { |
| for (uint32_t i = 0; i < numberOfLocals; i ++) { |
| uint32_t localIndex = m_locals.size(); |
| m_localSlots.append(allocateStack(Value::fromLocal(type.kind, localIndex))); |
| m_localAndCalleeSaveStorage = m_frameSizeForValidation; |
| m_locals.append(m_localSlots.last()); |
| m_localTypes.append(type.kind); |
| } |
| return { }; |
| } |
| |
| // Tables |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableSet(unsigned tableIndex, Value index, Value value) |
| { |
| // FIXME: Emit this inline <https://bugs.webkit.org/show_bug.cgi?id=198506>. |
| ASSERT(index.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(tableIndex), |
| index, |
| value |
| }; |
| |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationSetWasmTableElement, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("TableSet", tableIndex, index, value); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTableAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableInit(unsigned elementIndex, unsigned tableIndex, ExpressionType dstOffset, ExpressionType srcOffset, ExpressionType length) |
| { |
| ASSERT(dstOffset.type() == TypeKind::I32); |
| ASSERT(srcOffset.type() == TypeKind::I32); |
| ASSERT(length.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(elementIndex), |
| Value::fromI32(tableIndex), |
| dstOffset, |
| srcOffset, |
| length |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmTableInit, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("TableInit", tableIndex, dstOffset, srcOffset, length); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTableAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addElemDrop(unsigned elementIndex) |
| { |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(elementIndex) |
| }; |
| emitCCall(&operationWasmElemDrop, arguments); |
| |
| LOG_INSTRUCTION("ElemDrop", elementIndex); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableSize(unsigned tableIndex, Value& result) |
| { |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(tableIndex) |
| }; |
| result = topValue(TypeKind::I32); |
| emitCCall(&operationGetWasmTableSize, arguments, result); |
| |
| LOG_INSTRUCTION("TableSize", tableIndex, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableGrow(unsigned tableIndex, Value fill, Value delta, Value& result) |
| { |
| ASSERT(delta.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(tableIndex), |
| fill, delta |
| }; |
| result = topValue(TypeKind::I32); |
| emitCCall(&operationWasmTableGrow, arguments, result); |
| |
| LOG_INSTRUCTION("TableGrow", tableIndex, fill, delta, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableFill(unsigned tableIndex, Value offset, Value fill, Value count) |
| { |
| ASSERT(offset.type() == TypeKind::I32); |
| ASSERT(count.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(tableIndex), |
| offset, fill, count |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmTableFill, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("TableFill", tableIndex, fill, offset, count); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTableAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTableCopy(unsigned dstTableIndex, unsigned srcTableIndex, Value dstOffset, Value srcOffset, Value length) |
| { |
| ASSERT(dstOffset.type() == TypeKind::I32); |
| ASSERT(srcOffset.type() == TypeKind::I32); |
| ASSERT(length.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(dstTableIndex), |
| Value::fromI32(srcTableIndex), |
| dstOffset, srcOffset, length |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmTableCopy, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("TableCopy", dstTableIndex, srcTableIndex, dstOffset, srcOffset, length); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTableAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| // Locals |
| |
| [[nodiscard]] PartialResult BBQJIT::getLocal(uint32_t localIndex, Value& result) |
| { |
| // Currently, we load locals as temps, which basically prevents register allocation of locals. |
| // This is probably not ideal, we have infrastructure to support binding locals to registers, but |
| // we currently can't track local versioning (meaning we can get SSA-style issues where assigning |
| // to a local also updates all previous uses). |
| result = topValue(m_parser->typeOfLocal(localIndex).kind); |
| Location resultLocation = allocate(result); |
| emitLoad(Value::fromLocal(m_parser->typeOfLocal(localIndex).kind, localIndex), resultLocation); |
| LOG_INSTRUCTION("GetLocal", localIndex, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::setLocal(uint32_t localIndex, Value value) |
| { |
| if (!value.isConst()) |
| loadIfNecessary(value); |
| Value local = Value::fromLocal(m_parser->typeOfLocal(localIndex).kind, localIndex); |
| Location localLocation = locationOf(local); |
| emitStore(value, localLocation); |
| consume(value); |
| LOG_INSTRUCTION("SetLocal", localIndex, value); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::teeLocal(uint32_t localIndex, Value value, Value& result) |
| { |
| auto type = m_parser->typeOfLocal(localIndex); |
| Value local = Value::fromLocal(type.kind, localIndex); |
| if (value.isConst()) { |
| Location localLocation = locationOf(local); |
| emitStore(value, localLocation); |
| consume(value); |
| result = topValue(type.kind); |
| Location resultLocation = allocate(result); |
| emitMoveConst(value, resultLocation); |
| } else { |
| Location srcLocation = loadIfNecessary(value); |
| Location localLocation = locationOf(local); |
| emitStore(value, localLocation); |
| consume(value); |
| result = topValue(type.kind); |
| Location resultLocation = allocate(result); |
| emitMove(type.kind, srcLocation, resultLocation); |
| } |
| LOG_INSTRUCTION("TeeLocal", localIndex, value, RESULT(result)); |
| return { }; |
| } |
| |
| // Globals |
| |
| Value BBQJIT::topValue(TypeKind type, unsigned offset) |
| { |
| return Value::fromTemp(type, currentControlData().enclosedHeight() + currentControlData().implicitSlots() + m_parser->expressionStack().size() + offset); |
| } |
| |
| Value BBQJIT::exception(const ControlData& control) |
| { |
| ASSERT(ControlData::isAnyCatch(control)); |
| return Value::fromTemp(TypeKind::Externref, control.enclosedHeight()); |
| } |
| |
| void BBQJIT::emitWriteBarrier(GPRReg cellGPR) |
| { |
| GPRReg vmGPR; |
| GPRReg cellStateGPR; |
| { |
| ScratchScope<2, 0> scratches(*this); |
| vmGPR = scratches.gpr(0); |
| cellStateGPR = scratches.gpr(1); |
| } |
| |
| // We must flush everything first. Jumping over flush (emitCCall) is wrong since paths need to get merged. |
| flushRegisters(); |
| |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfVM()), vmGPR); |
| m_jit.load8(Address(cellGPR, JSCell::cellStateOffset()), cellStateGPR); |
| auto noFenceCheck = m_jit.branch32(RelationalCondition::Above, cellStateGPR, Address(vmGPR, VM::offsetOfHeapBarrierThreshold())); |
| |
| // Fence check path |
| auto toSlowPath = m_jit.branchTest8(ResultCondition::Zero, Address(vmGPR, VM::offsetOfHeapMutatorShouldBeFenced())); |
| |
| // Fence path |
| m_jit.memoryFence(); |
| Jump belowBlackThreshold = m_jit.branch8(RelationalCondition::Above, Address(cellGPR, JSCell::cellStateOffset()), TrustedImm32(blackThreshold)); |
| |
| // Slow path |
| toSlowPath.link(&m_jit); |
| m_jit.prepareWasmCallOperation(GPRInfo::wasmContextInstancePointer); |
| m_jit.setupArguments<decltype(operationWasmWriteBarrierSlowPath)>(cellGPR, vmGPR); |
| m_jit.callOperation<OperationPtrTag>(operationWasmWriteBarrierSlowPath); |
| |
| // Continuation |
| noFenceCheck.link(&m_jit); |
| belowBlackThreshold.link(&m_jit); |
| } |
| |
| void BBQJIT::emitMutatorFence() |
| { |
| if (isX86_64()) |
| return; |
| |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfVM()), wasmScratchGPR); |
| Jump ok = m_jit.branchTest8(MacroAssembler::Zero, Address(wasmScratchGPR, VM::offsetOfHeapMutatorShouldBeFenced())); |
| m_jit.storeFence(); |
| ok.link(m_jit); |
| } |
| |
| // Memory |
| |
| Address BBQJIT::materializePointer(Location pointerLocation, uint32_t uoffset) |
| { |
| if (static_cast<uint64_t>(uoffset) > static_cast<uint64_t>(std::numeric_limits<int32_t>::max()) || !B3::Air::Arg::isValidAddrForm(B3::Air::Move, static_cast<int32_t>(uoffset), Width::Width128)) { |
| m_jit.addPtr(TrustedImmPtr(static_cast<int64_t>(uoffset)), pointerLocation.asGPR()); |
| return Address(pointerLocation.asGPR()); |
| } |
| return Address(pointerLocation.asGPR(), static_cast<int32_t>(uoffset)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addGrowMemory(Value delta, Value& result, uint8_t memoryIndex) |
| { |
| Vector<Value, 8> arguments = { instanceValue(), delta, Value::fromI32(memoryIndex) }; |
| result = topValue(m_info.memory(memoryIndex).addressType().asWasmTypeKind()); |
| emitCCall(&operationGrowMemory, arguments, result); |
| restoreWebAssemblyGlobalState(); |
| |
| LOG_INSTRUCTION("GrowMemory", delta, RESULT(result)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCurrentMemory(Value& result, uint8_t memoryIndex) |
| { |
| result = topValue(m_info.memory(memoryIndex).addressType().asWasmTypeKind()); |
| if (!memoryIndex) { |
| Location resultLocation = allocate(result); |
| constexpr uint32_t shiftValue = 16; |
| static_assert(PageCount::pageSize == 1ull << shiftValue, "This must hold for the code below to be correct."); |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfCachedMemory0Size()), resultLocation.asGPR()); |
| m_jit.urshiftPtr(TrustedImm32(shiftValue), resultLocation.asGPR()); |
| if (!m_info.memory(memoryIndex).isMemory64()) |
| m_jit.zeroExtend32ToWord(resultLocation.asGPR(), resultLocation.asGPR()); |
| } else { |
| Vector<Value, 8> arguments = { instanceValue(), Value::fromI32(memoryIndex) }; |
| emitCCall(&operationWasmMemorySizeInPages, arguments, result); |
| restoreWebAssemblyGlobalState(); |
| } |
| |
| LOG_INSTRUCTION("CurrentMemory", RESULT(result)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addMemoryFill(Value dstAddress, Value targetValue, Value count, uint8_t memoryIndex) |
| { |
| ASSERT(dstAddress.type() == m_info.memory(memoryIndex).addressType()); |
| ASSERT(count.type() == m_info.memory(memoryIndex).addressType()); |
| ASSERT(targetValue.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| dstAddress, targetValue, count, Value::fromI32(memoryIndex) |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmMemoryFill, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| LOG_INSTRUCTION("MemoryFill", dstAddress, targetValue, count); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addMemoryCopy(Value dstAddress, Value srcAddress, Value count, uint8_t dstMemoryIndex, uint8_t srcMemoryIndex) |
| { |
| ASSERT(dstAddress.type() == m_info.memory(dstMemoryIndex).addressType()); |
| ASSERT(srcAddress.type() == m_info.memory(srcMemoryIndex).addressType()); |
| if (m_info.memory(dstMemoryIndex).isMemory64() && m_info.memory(srcMemoryIndex).isMemory64()) |
| ASSERT(count.type() == TypeKind::I64); |
| else |
| ASSERT(count.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| dstAddress, srcAddress, count, Value::fromI32(dstMemoryIndex), Value::fromI32(srcMemoryIndex) |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmMemoryCopy, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| LOG_INSTRUCTION("MemoryCopy", dstAddress, srcAddress, count); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addMemoryInit(unsigned dataSegmentIndex, Value dstAddress, Value srcAddress, Value length, uint8_t memoryIndex) |
| { |
| ASSERT(dstAddress.type() == m_info.memory(memoryIndex).addressType()); |
| ASSERT(srcAddress.type() == TypeKind::I32); |
| ASSERT(length.type() == TypeKind::I32); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(dataSegmentIndex), |
| dstAddress, srcAddress, length, Value::fromI32(memoryIndex) |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmMemoryInit, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| LOG_INSTRUCTION("MemoryInit", dataSegmentIndex, dstAddress, srcAddress, length); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addDataDrop(unsigned dataSegmentIndex) |
| { |
| Vector<Value, 8> arguments = { instanceValue(), Value::fromI32(dataSegmentIndex) }; |
| emitCCall(&operationWasmDataDrop, arguments); |
| |
| LOG_INSTRUCTION("DataDrop", dataSegmentIndex); |
| return { }; |
| } |
| |
| // Atomics |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicLoad(ExtAtomicOpType loadOp, Type valueType, ExpressionType pointer, ExpressionType& result, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| if (sumOverflows<uint32_t>(uoffset, sizeOfAtomicOpMemoryAccess(loadOp))) [[unlikely]] { |
| // FIXME: Same issue as in AirIRGenerator::load(): https://bugs.webkit.org/show_bug.cgi?id=166435 |
| emitThrowException(ExceptionType::OutOfBoundsMemoryAccess); |
| consume(pointer); |
| result = valueType.isI64() ? Value::fromI64(0) : Value::fromI32(0); |
| } else |
| result = emitAtomicLoadOp(loadOp, valueType, emitCheckAndPreparePointer(pointer, uoffset, sizeOfAtomicOpMemoryAccess(loadOp), memoryIndex), uoffset); |
| |
| LOG_INSTRUCTION(makeString(loadOp), pointer, uoffset, RESULT(result)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicStore(ExtAtomicOpType storeOp, Type valueType, ExpressionType pointer, ExpressionType value, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| Location valueLocation = locationOf(value); |
| if (sumOverflows<uint32_t>(uoffset, sizeOfAtomicOpMemoryAccess(storeOp))) [[unlikely]] { |
| // FIXME: Same issue as in AirIRGenerator::load(): https://bugs.webkit.org/show_bug.cgi?id=166435 |
| emitThrowException(ExceptionType::OutOfBoundsMemoryAccess); |
| consume(pointer); |
| consume(value); |
| } else |
| emitAtomicStoreOp(storeOp, valueType, emitCheckAndPreparePointer(pointer, uoffset, sizeOfAtomicOpMemoryAccess(storeOp), memoryIndex), value, uoffset); |
| |
| LOG_INSTRUCTION(makeString(storeOp), pointer, uoffset, value, valueLocation); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicBinaryRMW(ExtAtomicOpType op, Type valueType, ExpressionType pointer, ExpressionType value, ExpressionType& result, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| Location valueLocation = locationOf(value); |
| if (sumOverflows<uint32_t>(uoffset, sizeOfAtomicOpMemoryAccess(op))) [[unlikely]] { |
| // FIXME: Even though this is provably out of bounds, it's not a validation error, so we have to handle it |
| // as a runtime exception. However, this may change: https://bugs.webkit.org/show_bug.cgi?id=166435 |
| emitThrowException(ExceptionType::OutOfBoundsMemoryAccess); |
| consume(pointer); |
| consume(value); |
| result = valueType.isI64() ? Value::fromI64(0) : Value::fromI32(0); |
| } else |
| result = emitAtomicBinaryRMWOp(op, valueType, emitCheckAndPreparePointer(pointer, uoffset, sizeOfAtomicOpMemoryAccess(op), memoryIndex), value, uoffset); |
| |
| LOG_INSTRUCTION(makeString(op), pointer, uoffset, value, valueLocation, RESULT(result)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicCompareExchange(ExtAtomicOpType op, Type valueType, ExpressionType pointer, ExpressionType expected, ExpressionType value, ExpressionType& result, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| Location valueLocation = locationOf(value); |
| if (sumOverflows<uint32_t>(uoffset, sizeOfAtomicOpMemoryAccess(op))) [[unlikely]] { |
| // FIXME: Even though this is provably out of bounds, it's not a validation error, so we have to handle it |
| // as a runtime exception. However, this may change: https://bugs.webkit.org/show_bug.cgi?id=166435 |
| emitThrowException(ExceptionType::OutOfBoundsMemoryAccess); |
| consume(pointer); |
| consume(expected); |
| consume(value); |
| result = valueType.isI64() ? Value::fromI64(0) : Value::fromI32(0); |
| } else |
| result = emitAtomicCompareExchange(op, valueType, emitCheckAndPreparePointer(pointer, uoffset, sizeOfAtomicOpMemoryAccess(op), memoryIndex), expected, value, uoffset); |
| |
| LOG_INSTRUCTION(makeString(op), pointer, expected, value, valueLocation, uoffset, RESULT(result)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicWait(ExtAtomicOpType op, ExpressionType pointer, ExpressionType value, ExpressionType timeout, ExpressionType& result, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| pointer, |
| Value::fromI32(uoffset), |
| value, |
| timeout, |
| Value::fromI32(memoryIndex) |
| }; |
| |
| result = topValue(TypeKind::I32); |
| if (op == ExtAtomicOpType::MemoryAtomicWait32) |
| emitCCall(&operationMemoryAtomicWait32, arguments, result); |
| else |
| emitCCall(&operationMemoryAtomicWait64, arguments, result); |
| Location resultLocation = loadIfNecessary(result); |
| |
| LOG_INSTRUCTION(makeString(op), pointer, value, timeout, uoffset, RESULT(result)); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branch32(RelationalCondition::LessThan, resultLocation.asGPR(), TrustedImm32(0))); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicNotify(ExtAtomicOpType op, ExpressionType pointer, ExpressionType count, ExpressionType& result, uint32_t uoffset, uint8_t memoryIndex) |
| { |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| pointer, |
| Value::fromI32(uoffset), |
| count, |
| Value::fromI32(memoryIndex) |
| }; |
| result = topValue(TypeKind::I32); |
| emitCCall(&operationMemoryAtomicNotify, arguments, result); |
| Location resultLocation = loadIfNecessary(result); |
| |
| LOG_INSTRUCTION(makeString(op), pointer, count, uoffset, RESULT(result)); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branch32(RelationalCondition::LessThan, resultLocation.asGPR(), TrustedImm32(0))); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::atomicFence(ExtAtomicOpType, uint8_t) |
| { |
| m_jit.memoryFence(); |
| return { }; |
| } |
| |
| // Saturated truncation. |
| |
| TruncationKind BBQJIT::truncationKind(OpType truncationOp) |
| { |
| switch (truncationOp) { |
| case OpType::I32TruncSF32: |
| return TruncationKind::I32TruncF32S; |
| case OpType::I32TruncUF32: |
| return TruncationKind::I32TruncF32U; |
| case OpType::I64TruncSF32: |
| return TruncationKind::I64TruncF32S; |
| case OpType::I64TruncUF32: |
| return TruncationKind::I64TruncF32U; |
| case OpType::I32TruncSF64: |
| return TruncationKind::I32TruncF64S; |
| case OpType::I32TruncUF64: |
| return TruncationKind::I32TruncF64U; |
| case OpType::I64TruncSF64: |
| return TruncationKind::I64TruncF64S; |
| case OpType::I64TruncUF64: |
| return TruncationKind::I64TruncF64U; |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Not a truncation op"); |
| } |
| } |
| |
| TruncationKind BBQJIT::truncationKind(Ext1OpType truncationOp) |
| { |
| switch (truncationOp) { |
| case Ext1OpType::I32TruncSatF32S: |
| return TruncationKind::I32TruncF32S; |
| case Ext1OpType::I32TruncSatF32U: |
| return TruncationKind::I32TruncF32U; |
| case Ext1OpType::I64TruncSatF32S: |
| return TruncationKind::I64TruncF32S; |
| case Ext1OpType::I64TruncSatF32U: |
| return TruncationKind::I64TruncF32U; |
| case Ext1OpType::I32TruncSatF64S: |
| return TruncationKind::I32TruncF64S; |
| case Ext1OpType::I32TruncSatF64U: |
| return TruncationKind::I32TruncF64U; |
| case Ext1OpType::I64TruncSatF64S: |
| return TruncationKind::I64TruncF64S; |
| case Ext1OpType::I64TruncSatF64U: |
| return TruncationKind::I64TruncF64U; |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Not a truncation op"); |
| } |
| } |
| |
| FloatingPointRange BBQJIT::lookupTruncationRange(TruncationKind truncationKind) |
| { |
| Value min; |
| Value max; |
| bool closedLowerEndpoint = false; |
| |
| switch (truncationKind) { |
| case TruncationKind::I32TruncF32S: |
| closedLowerEndpoint = true; |
| max = Value::fromF32(-static_cast<float>(std::numeric_limits<int32_t>::min())); |
| min = Value::fromF32(static_cast<float>(std::numeric_limits<int32_t>::min())); |
| break; |
| case TruncationKind::I32TruncF32U: |
| max = Value::fromF32(static_cast<float>(std::numeric_limits<int32_t>::min()) * static_cast<float>(-2.0)); |
| min = Value::fromF32(static_cast<float>(-1.0)); |
| break; |
| case TruncationKind::I32TruncF64S: |
| max = Value::fromF64(-static_cast<double>(std::numeric_limits<int32_t>::min())); |
| min = Value::fromF64(static_cast<double>(std::numeric_limits<int32_t>::min()) - 1.0); |
| break; |
| case TruncationKind::I32TruncF64U: |
| max = Value::fromF64(static_cast<double>(std::numeric_limits<int32_t>::min()) * -2.0); |
| min = Value::fromF64(-1.0); |
| break; |
| case TruncationKind::I64TruncF32S: |
| closedLowerEndpoint = true; |
| max = Value::fromF32(-static_cast<float>(std::numeric_limits<int64_t>::min())); |
| min = Value::fromF32(static_cast<float>(std::numeric_limits<int64_t>::min())); |
| break; |
| case TruncationKind::I64TruncF32U: |
| max = Value::fromF32(static_cast<float>(std::numeric_limits<int64_t>::min()) * static_cast<float>(-2.0)); |
| min = Value::fromF32(static_cast<float>(-1.0)); |
| break; |
| case TruncationKind::I64TruncF64S: |
| closedLowerEndpoint = true; |
| max = Value::fromF64(-static_cast<double>(std::numeric_limits<int64_t>::min())); |
| min = Value::fromF64(static_cast<double>(std::numeric_limits<int64_t>::min())); |
| break; |
| case TruncationKind::I64TruncF64U: |
| max = Value::fromF64(static_cast<double>(std::numeric_limits<int64_t>::min()) * -2.0); |
| min = Value::fromF64(-1.0); |
| break; |
| } |
| |
| return FloatingPointRange { min, max, closedLowerEndpoint }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::truncTrapping(OpType truncationOp, Value operand, Value& result, Type returnType, Type operandType) |
| { |
| ScratchScope<0, 2> scratches(*this); |
| |
| Location operandLocation; |
| if (operand.isConst()) { |
| operandLocation = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(operand, operandLocation); |
| } else |
| operandLocation = loadIfNecessary(operand); |
| ASSERT(operandLocation.isRegister()); |
| |
| consume(operand); // Allow temp operand location to be reused |
| |
| result = topValue(returnType.kind); |
| Location resultLocation = allocate(result); |
| TruncationKind kind = truncationKind(truncationOp); |
| auto range = lookupTruncationRange(kind); |
| auto minFloatConst = range.min; |
| auto maxFloatConst = range.max; |
| Location minFloat = Location::fromFPR(scratches.fpr(0)); |
| Location maxFloat = Location::fromFPR(scratches.fpr(1)); |
| |
| // FIXME: Can we do better isel here? Two floating-point constant materializations for every |
| // trunc seems costly. |
| emitMoveConst(minFloatConst, minFloat); |
| emitMoveConst(maxFloatConst, maxFloat); |
| |
| LOG_INSTRUCTION("TruncTrapping", operand, operandLocation, RESULT(result)); |
| |
| DoubleCondition minCondition = range.closedLowerEndpoint ? DoubleCondition::DoubleLessThanOrUnordered : DoubleCondition::DoubleLessThanOrEqualOrUnordered; |
| Jump belowMin = operandType == Types::F32 |
| ? m_jit.branchFloat(minCondition, operandLocation.asFPR(), minFloat.asFPR()) |
| : m_jit.branchDouble(minCondition, operandLocation.asFPR(), minFloat.asFPR()); |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTrunc, belowMin); |
| |
| Jump aboveMax = operandType == Types::F32 |
| ? m_jit.branchFloat(DoubleCondition::DoubleGreaterThanOrEqualOrUnordered, operandLocation.asFPR(), maxFloat.asFPR()) |
| : m_jit.branchDouble(DoubleCondition::DoubleGreaterThanOrEqualOrUnordered, operandLocation.asFPR(), maxFloat.asFPR()); |
| recordJumpToThrowException(ExceptionType::OutOfBoundsTrunc, aboveMax); |
| |
| truncInBounds(kind, operandLocation, resultLocation, scratches.fpr(0), scratches.fpr(1)); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::truncSaturated(Ext1OpType truncationOp, Value operand, Value& result, Type returnType, Type operandType) |
| { |
| TruncationKind kind = truncationKind(truncationOp); |
| |
| if constexpr (isARM64()) { |
| // ARM64 FCVTZS/FCVTZU natively implement WebAssembly trunc-sat semantics: |
| // NaN -> 0, out-of-range -> saturated min/max. |
| Location operandLocation; |
| if (operand.isConst()) { |
| operandLocation = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(operand, operandLocation); |
| } else |
| operandLocation = loadIfNecessary(operand); |
| ASSERT(operandLocation.isRegister()); |
| |
| consume(operand); |
| |
| result = topValue(returnType.kind); |
| Location resultLocation = allocate(result); |
| |
| LOG_INSTRUCTION("TruncSaturated", operand, operandLocation, RESULT(result)); |
| |
| truncInBounds(kind, operandLocation, resultLocation, InvalidFPRReg, InvalidFPRReg); |
| return { }; |
| } |
| |
| ScratchScope<0, 2> scratches(*this); |
| |
| auto range = lookupTruncationRange(kind); |
| auto minFloatConst = range.min; |
| auto maxFloatConst = range.max; |
| Location minFloat = Location::fromFPR(scratches.fpr(0)); |
| Location maxFloat = Location::fromFPR(scratches.fpr(1)); |
| |
| // FIXME: Can we do better isel here? Two floating-point constant materializations for every |
| // trunc seems costly. |
| emitMoveConst(minFloatConst, minFloat); |
| emitMoveConst(maxFloatConst, maxFloat); |
| |
| // Determine min/max integer results for saturation. |
| uint64_t minResult = 0; |
| uint64_t maxResult = 0; |
| switch (kind) { |
| case TruncationKind::I32TruncF32S: |
| case TruncationKind::I32TruncF64S: |
| maxResult = std::bit_cast<uint32_t>(INT32_MAX); |
| minResult = std::bit_cast<uint32_t>(INT32_MIN); |
| break; |
| case TruncationKind::I32TruncF32U: |
| case TruncationKind::I32TruncF64U: |
| maxResult = std::bit_cast<uint32_t>(UINT32_MAX); |
| minResult = std::bit_cast<uint32_t>(0U); |
| break; |
| case TruncationKind::I64TruncF32S: |
| case TruncationKind::I64TruncF64S: |
| maxResult = std::bit_cast<uint64_t>(INT64_MAX); |
| minResult = std::bit_cast<uint64_t>(INT64_MIN); |
| break; |
| case TruncationKind::I64TruncF32U: |
| case TruncationKind::I64TruncF64U: |
| maxResult = std::bit_cast<uint64_t>(UINT64_MAX); |
| minResult = std::bit_cast<uint64_t>(0ULL); |
| break; |
| } |
| |
| Location operandLocation; |
| if (operand.isConst()) { |
| operandLocation = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(operand, operandLocation); |
| } else |
| operandLocation = loadIfNecessary(operand); |
| ASSERT(operandLocation.isRegister()); |
| |
| consume(operand); // Allow temp operand location to be reused |
| |
| result = topValue(returnType.kind); |
| Location resultLocation = allocate(result); |
| |
| LOG_INSTRUCTION("TruncSaturated", operand, operandLocation, RESULT(result)); |
| |
| Jump lowerThanMin = operandType == Types::F32 |
| ? m_jit.branchFloat(DoubleCondition::DoubleLessThanOrEqualOrUnordered, operandLocation.asFPR(), minFloat.asFPR()) |
| : m_jit.branchDouble(DoubleCondition::DoubleLessThanOrEqualOrUnordered, operandLocation.asFPR(), minFloat.asFPR()); |
| Jump higherThanMax = operandType == Types::F32 |
| ? m_jit.branchFloat(DoubleCondition::DoubleGreaterThanOrEqualOrUnordered, operandLocation.asFPR(), maxFloat.asFPR()) |
| : m_jit.branchDouble(DoubleCondition::DoubleGreaterThanOrEqualOrUnordered, operandLocation.asFPR(), maxFloat.asFPR()); |
| |
| // In-bounds case. Emit normal truncation instructions. |
| truncInBounds(kind, operandLocation, resultLocation, scratches.fpr(0), scratches.fpr(1)); |
| |
| Jump afterInBounds = m_jit.jump(); |
| |
| // Below-minimum case. |
| lowerThanMin.link(&m_jit); |
| |
| // As an optimization, if the min result is 0; we can unconditionally return |
| // that if the above-minimum-range check fails; otherwise, we need to check |
| // for NaN since it also will fail the above-minimum-range-check |
| if (!minResult) { |
| // Use emitMoveConst to handle 32-bit and 64-bit uniformly. |
| emitMoveConst(returnType == Types::I32 ? Value::fromI32(0) : Value::fromI64(0), resultLocation); |
| } else { |
| Jump isNotNaN = operandType == Types::F32 |
| ? m_jit.branchFloat(DoubleCondition::DoubleEqualAndOrdered, operandLocation.asFPR(), operandLocation.asFPR()) |
| : m_jit.branchDouble(DoubleCondition::DoubleEqualAndOrdered, operandLocation.asFPR(), operandLocation.asFPR()); |
| |
| // NaN case. Set result to zero. |
| emitMoveConst(returnType == Types::I32 ? Value::fromI32(0) : Value::fromI64(0), resultLocation); |
| Jump afterNaN = m_jit.jump(); |
| |
| // Non-NaN case. Set result to the minimum value. |
| isNotNaN.link(&m_jit); |
| emitMoveConst(returnType == Types::I32 ? Value::fromI32(static_cast<int32_t>(minResult)) : Value::fromI64(static_cast<int64_t>(minResult)), resultLocation); |
| afterNaN.link(&m_jit); |
| } |
| Jump afterMin = m_jit.jump(); |
| |
| // Above maximum case. |
| higherThanMax.link(&m_jit); |
| emitMoveConst(returnType == Types::I32 ? Value::fromI32(static_cast<int32_t>(maxResult)) : Value::fromI64(static_cast<int64_t>(maxResult)), resultLocation); |
| |
| afterInBounds.link(&m_jit); |
| afterMin.link(&m_jit); |
| |
| return { }; |
| } |
| |
| // GC |
| |
| // Given a type index for an array signature, look it up, expand it and |
| // return the element type |
| StorageType BBQJIT::getArrayElementType(TypeSignatureIndex typeIndex) |
| { |
| const RTT& arrayRTT = m_info.rtt(typeIndex); |
| ASSERT(arrayRTT.kind() == RTTKind::Array); |
| return arrayRTT.elementType().type; |
| } |
| |
| void BBQJIT::pushArrayNewFromSegment(ArraySegmentOperation operation, TypeSignatureIndex typeIndex, uint32_t segmentIndex, ExpressionType arraySize, ExpressionType offset, ExceptionType exceptionType, ExpressionType& result) |
| { |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(typeIndex.rawIndex()), |
| Value::fromI32(segmentIndex), |
| arraySize, |
| offset, |
| }; |
| result = topValue(TypeKind::I64); |
| emitCCall(operation, arguments, result); |
| Location resultLocation = loadIfNecessary(result); |
| |
| emitThrowOnNullReference(exceptionType, resultLocation); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addArrayNewData(TypeSignatureIndex typeIndex, uint32_t dataIndex, ExpressionType arraySize, ExpressionType offset, ExpressionType& result) |
| { |
| pushArrayNewFromSegment(operationWasmArrayNewData, typeIndex, dataIndex, arraySize, offset, ExceptionType::BadArrayNewInitData, result); |
| LOG_INSTRUCTION("ArrayNewData", typeIndex, dataIndex, arraySize, offset, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addArrayNewElem(TypeSignatureIndex typeIndex, uint32_t elemSegmentIndex, ExpressionType arraySize, ExpressionType offset, ExpressionType& result) |
| { |
| pushArrayNewFromSegment(operationWasmArrayNewElem, typeIndex, elemSegmentIndex, arraySize, offset, ExceptionType::BadArrayNewInitElem, result); |
| LOG_INSTRUCTION("ArrayNewElem", typeIndex, elemSegmentIndex, arraySize, offset, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addArrayCopy(TypeSignatureIndex dstTypeIndex, TypedExpression typedDst, ExpressionType dstOffset, TypeSignatureIndex srcTypeIndex, TypedExpression typedSrc, ExpressionType srcOffset, ExpressionType size) |
| { |
| auto dst = typedDst.value(); |
| auto src = typedSrc.value(); |
| if (dst.isConst() || src.isConst()) { |
| ASSERT_IMPLIES(dst.isConst(), dst.asI64() == JSValue::encode(jsNull())); |
| ASSERT_IMPLIES(src.isConst(), src.asI64() == JSValue::encode(jsNull())); |
| |
| LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size); |
| |
| consume(dst); |
| consume(dstOffset); |
| consume(src); |
| consume(srcOffset); |
| consume(size); |
| emitThrowException(ExceptionType::NullArrayCopy); |
| return { }; |
| } |
| |
| if (typedDst.type().isNullable()) |
| emitThrowOnNullReference(ExceptionType::NullArrayCopy, loadIfNecessary(dst)); |
| if (typedSrc.type().isNullable()) |
| emitThrowOnNullReference(ExceptionType::NullArrayCopy, loadIfNecessary(src)); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| dst, |
| dstOffset, |
| src, |
| srcOffset, |
| size |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmArrayCopy, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsArrayCopy, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addArrayInitElem(TypeSignatureIndex dstTypeIndex, TypedExpression typedDst, ExpressionType dstOffset, uint32_t srcElementIndex, ExpressionType srcOffset, ExpressionType size) |
| { |
| auto dst = typedDst.value(); |
| if (dst.isConst()) { |
| ASSERT(dst.asI64() == JSValue::encode(jsNull())); |
| |
| LOG_INSTRUCTION("ArrayInitElem", dstTypeIndex, dst, dstOffset, srcElementIndex, srcOffset, size); |
| |
| consume(dstOffset); |
| consume(srcOffset); |
| consume(size); |
| emitThrowException(ExceptionType::NullArrayInitElem); |
| return { }; |
| } |
| |
| if (typedDst.type().isNullable()) |
| emitThrowOnNullReference(ExceptionType::NullArrayInitElem, loadIfNecessary(dst)); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| dst, |
| dstOffset, |
| Value::fromI32(srcElementIndex), |
| srcOffset, |
| size |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmArrayInitElem, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("ArrayInitElem", dstTypeIndex, dst, dstOffset, srcElementIndex, srcOffset, size); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsArrayInitElem, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addArrayInitData(TypeSignatureIndex dstTypeIndex, TypedExpression typedDst, ExpressionType dstOffset, uint32_t srcDataIndex, ExpressionType srcOffset, ExpressionType size) |
| { |
| auto dst = typedDst.value(); |
| if (dst.isConst()) { |
| ASSERT(dst.asI64() == JSValue::encode(jsNull())); |
| |
| LOG_INSTRUCTION("ArrayInitData", dstTypeIndex, dst, dstOffset, srcDataIndex, srcOffset, size); |
| |
| consume(dstOffset); |
| consume(srcOffset); |
| consume(size); |
| emitThrowException(ExceptionType::NullArrayInitData); |
| return { }; |
| } |
| |
| if (typedDst.type().isNullable()) |
| emitThrowOnNullReference(ExceptionType::NullArrayInitData, loadIfNecessary(dst)); |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| dst, |
| dstOffset, |
| Value::fromI32(srcDataIndex), |
| srcOffset, |
| size |
| }; |
| Value shouldThrow = topValue(TypeKind::I32); |
| emitCCall(&operationWasmArrayInitData, arguments, shouldThrow); |
| Location shouldThrowLocation = loadIfNecessary(shouldThrow); |
| |
| LOG_INSTRUCTION("ArrayInitData", dstTypeIndex, dst, dstOffset, srcDataIndex, srcOffset, size); |
| |
| recordJumpToThrowException(ExceptionType::OutOfBoundsArrayInitData, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR())); |
| |
| consume(shouldThrow); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addAnyConvertExtern(ExpressionType reference, ExpressionType& result) |
| { |
| Vector<Value, 8> arguments = { |
| reference |
| }; |
| result = topValue(TypeKind::Anyref); |
| emitCCall(&operationWasmAnyConvertExtern, arguments, result); |
| |
| LOG_INSTRUCTION("AnyConvertExtern", reference, RESULT(result)); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addExternConvertAny(ExpressionType reference, ExpressionType& result) |
| { |
| auto referenceLocation = reference.isConst() ? Location::none() : loadIfNecessary(reference); |
| consume(reference); |
| |
| result = topValue(TypeKind::Externref); |
| auto resultLocation = allocate(result); |
| if (reference.isConst()) |
| emitMoveConst(reference, resultLocation); |
| else |
| emitMove(reference.type(), referenceLocation, resultLocation); |
| |
| LOG_INSTRUCTION("ExternConvertAny", reference, RESULT(result)); |
| return { }; |
| } |
| |
| // Basic operators |
| [[nodiscard]] PartialResult BBQJIT::addSelect(Value condition, Value lhs, Value rhs, Value& result) |
| { |
| if (condition.isConst()) { |
| Value src = condition.asI32() ? lhs : rhs; |
| Location srcLocation; |
| if (src.isConst()) |
| result = src; |
| else { |
| result = topValue(lhs.type()); |
| srcLocation = loadIfNecessary(src); |
| } |
| |
| LOG_INSTRUCTION("Select", condition, lhs, rhs, RESULT(result)); |
| consume(condition); |
| consume(lhs); |
| consume(rhs); |
| if (!result.isConst()) { |
| Location resultLocation = allocate(result); |
| emitMove(lhs.type(), srcLocation, resultLocation); |
| } |
| } else { |
| Location conditionLocation = loadIfNecessary(condition); |
| Location lhsLocation = Location::none(), rhsLocation = Location::none(); |
| |
| // Ensure all non-constant parameters are loaded into registers. |
| if (!lhs.isConst()) |
| lhsLocation = loadIfNecessary(lhs); |
| if (!rhs.isConst()) |
| rhsLocation = loadIfNecessary(rhs); |
| |
| ASSERT(lhs.isConst() || lhsLocation.isRegister()); |
| ASSERT(rhs.isConst() || rhsLocation.isRegister()); |
| consume(lhs); |
| consume(rhs); |
| |
| result = topValue(lhs.type()); |
| Location resultLocation = allocate(result); |
| LOG_INSTRUCTION("Select", condition, lhs, lhsLocation, rhs, rhsLocation, RESULT(result)); |
| LOG_INDENT(); |
| |
| TypeKind type = lhs.type(); |
| if (!lhs.isConst() && !rhs.isConst() && type != TypeKind::V128) { |
| consume(condition); |
| if (type == TypeKind::F32 || type == TypeKind::F64) |
| m_jit.moveDoubleConditionallyTest32(ResultCondition::NonZero, conditionLocation.asGPR(), conditionLocation.asGPR(), lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| else |
| m_jit.moveConditionallyTest32(ResultCondition::NonZero, conditionLocation.asGPR(), conditionLocation.asGPR(), lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| } else { |
| bool inverted = false; |
| |
| // If the operands or the result alias, we want the matching one to be on top. |
| if (rhsLocation == resultLocation) { |
| std::swap(lhs, rhs); |
| std::swap(lhsLocation, rhsLocation); |
| inverted = true; |
| } |
| |
| // If the condition location and the result alias, we want to make sure the condition is |
| // preserved no matter what. |
| if (conditionLocation == resultLocation) { |
| m_jit.move(conditionLocation.asGPR(), wasmScratchGPR); |
| conditionLocation = Location::fromGPR(wasmScratchGPR); |
| } |
| |
| // Kind of gross isel, but it should handle all use/def aliasing cases correctly. |
| if (lhs.isConst()) |
| emitMoveConst(lhs, resultLocation); |
| else |
| emitMove(lhs.type(), lhsLocation, resultLocation); |
| Jump ifZero = m_jit.branchTest32(inverted ? ResultCondition::Zero : ResultCondition::NonZero, conditionLocation.asGPR(), conditionLocation.asGPR()); |
| consume(condition); |
| if (rhs.isConst()) |
| emitMoveConst(rhs, resultLocation); |
| else |
| emitMove(rhs.type(), rhsLocation, resultLocation); |
| ifZero.link(&m_jit); |
| } |
| |
| LOG_DEDENT(); |
| } |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Add(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32Add", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() + rhs.asI32())), |
| BLOCK( |
| m_jit.add32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| m_jit.add32(Imm32(ImmHelpers::imm(lhs, rhs).asI32()), ImmHelpers::regLocation(lhsLocation, rhsLocation).asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Add(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Add", TypeKind::F32, |
| BLOCK(Value::fromF32(lhs.asF32() + rhs.asF32())), |
| BLOCK( |
| m_jit.addFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.addFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Add(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Add", TypeKind::F64, |
| BLOCK(Value::fromF64(lhs.asF64() + rhs.asF64())), |
| BLOCK( |
| m_jit.addDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.addDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Sub(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32Sub", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() - rhs.asI32())), |
| BLOCK( |
| m_jit.sub32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.sub32(lhsLocation.asGPR(), TrustedImm32(rhs.asI32()), resultLocation.asGPR()); |
| else { |
| emitMoveConst(lhs, Location::fromGPR(wasmScratchGPR)); |
| m_jit.sub32(wasmScratchGPR, rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Sub(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Sub", TypeKind::F32, |
| BLOCK(Value::fromF32(lhs.asF32() - rhs.asF32())), |
| BLOCK( |
| m_jit.subFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) { |
| // If rhs is a constant, it will be expressed as a positive |
| // value and so needs to be negated unless it is NaN |
| auto floatVal = std::isnan(rhs.asF32()) ? rhs.asF32() : -rhs.asF32(); |
| emitMoveConst(Value::fromF32(floatVal), Location::fromFPR(wasmScratchFPR)); |
| m_jit.addFloat(lhsLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| } else { |
| emitMoveConst(lhs, Location::fromFPR(wasmScratchFPR)); |
| m_jit.subFloat(wasmScratchFPR, rhsLocation.asFPR(), resultLocation.asFPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Sub(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Sub", TypeKind::F64, |
| BLOCK(Value::fromF64(lhs.asF64() - rhs.asF64())), |
| BLOCK( |
| m_jit.subDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) { |
| // If rhs is a constant, it will be expressed as a positive |
| // value and so needs to be negated unless it is NaN |
| auto floatVal = std::isnan(rhs.asF64()) ? rhs.asF64() : -rhs.asF64(); |
| emitMoveConst(Value::fromF64(floatVal), Location::fromFPR(wasmScratchFPR)); |
| m_jit.addDouble(lhsLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| } else { |
| emitMoveConst(lhs, Location::fromFPR(wasmScratchFPR)); |
| m_jit.subDouble(wasmScratchFPR, rhsLocation.asFPR(), resultLocation.asFPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Mul(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32Mul", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() * rhs.asI32())), |
| BLOCK( |
| m_jit.mul32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| m_jit.mul32( |
| Imm32(ImmHelpers::imm(lhs, rhs).asI32()), |
| ImmHelpers::regLocation(lhsLocation, rhsLocation).asGPR(), |
| resultLocation.asGPR() |
| ); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Mul(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Mul", TypeKind::F32, |
| BLOCK(Value::fromF32(lhs.asF32() * rhs.asF32())), |
| BLOCK( |
| m_jit.mulFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.mulFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Mul(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Mul", TypeKind::F64, |
| BLOCK(Value::fromF64(lhs.asF64() * rhs.asF64())), |
| BLOCK( |
| m_jit.mulDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.mulDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| template<typename Func> |
| void BBQJIT::addLatePath(WasmOrigin origin, Func&& func) |
| { |
| m_latePaths.append({ origin, WTF::move(func) }); |
| } |
| |
| void BBQJIT::emitThrowException(ExceptionType type) |
| { |
| m_jit.move(CCallHelpers::TrustedImm32(static_cast<uint32_t>(type)), GPRInfo::argumentGPR1); |
| m_jit.jumpThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(throwExceptionFromWasmThunkGenerator).code())); |
| } |
| |
| void BBQJIT::recordJumpToThrowException(ExceptionType type, Jump jump) |
| { |
| m_exceptions[static_cast<unsigned>(type)].append(jump); |
| } |
| |
| void BBQJIT::recordJumpToThrowException(ExceptionType type, const JumpList& jumps) |
| { |
| m_exceptions[static_cast<unsigned>(type)].append(jumps); |
| } |
| |
| template<typename IntType, BBQJIT::ConstantDivOverflow overflow> |
| Value BBQJIT::checkConstantDivision(const Value& lhs, const Value& rhs) |
| { |
| constexpr bool is32 = sizeof(IntType) == 4; |
| int64_t lhsValue = is32 ? int64_t(lhs.asI32()) : lhs.asI64(); |
| int64_t rhsValue = is32 ? int64_t(rhs.asI32()) : rhs.asI64(); |
| |
| if (!rhsValue) { |
| emitThrowException(ExceptionType::DivisionByZero); |
| return is32 ? Value::fromI32(1) : Value::fromI64(1); |
| } |
| if constexpr (std::is_signed_v<IntType>) { |
| if (lhsValue == std::numeric_limits<IntType>::min() && rhsValue == -1) { |
| if constexpr (overflow == ConstantDivOverflow::CanOverflow) |
| emitThrowException(ExceptionType::IntegerOverflow); |
| // Wasm rem_s(INT_MIN, -1) is defined to return 0; substitute divisor to avoid C++ UB. |
| return is32 ? Value::fromI32(1) : Value::fromI64(1); |
| } |
| } |
| return rhs; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32DivS(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I32DivS", TypeKind::I32, |
| BLOCK( |
| Value::fromI32(lhs.asI32() / checkConstantDivision<int32_t, ConstantDivOverflow::CanOverflow>(lhs, rhs).asI32()) |
| ), |
| BLOCK( |
| emitModOrDiv<int32_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<int32_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64DivS(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I64DivS", TypeKind::I64, |
| BLOCK( |
| Value::fromI64(lhs.asI64() / checkConstantDivision<int64_t, ConstantDivOverflow::CanOverflow>(lhs, rhs).asI64()) |
| ), |
| BLOCK( |
| emitModOrDiv<int64_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<int64_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32DivU(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I32DivU", TypeKind::I32, |
| BLOCK( |
| Value::fromI32(static_cast<uint32_t>(lhs.asI32()) / static_cast<uint32_t>(checkConstantDivision<uint32_t>(lhs, rhs).asI32())) |
| ), |
| BLOCK( |
| emitModOrDiv<uint32_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<uint32_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64DivU(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I64DivU", TypeKind::I64, |
| BLOCK( |
| Value::fromI64(static_cast<uint64_t>(lhs.asI64()) / static_cast<uint64_t>(checkConstantDivision<uint64_t>(lhs, rhs).asI64())) |
| ), |
| BLOCK( |
| emitModOrDiv<uint64_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<uint64_t, false>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32RemS(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I32RemS", TypeKind::I32, |
| BLOCK( |
| Value::fromI32(lhs.asI32() % checkConstantDivision<int32_t>(lhs, rhs).asI32()) |
| ), |
| BLOCK( |
| emitModOrDiv<int32_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<int32_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64RemS(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I64RemS", TypeKind::I64, |
| BLOCK( |
| Value::fromI64(lhs.asI64() % checkConstantDivision<int64_t>(lhs, rhs).asI64()) |
| ), |
| BLOCK( |
| emitModOrDiv<int64_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<int64_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32RemU(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I32RemU", TypeKind::I32, |
| BLOCK( |
| Value::fromI32(static_cast<uint32_t>(lhs.asI32()) % static_cast<uint32_t>(checkConstantDivision<uint32_t>(lhs, rhs).asI32())) |
| ), |
| BLOCK( |
| emitModOrDiv<uint32_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<uint32_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64RemU(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_MOD_OR_DIV; |
| EMIT_BINARY( |
| "I64RemU", TypeKind::I64, |
| BLOCK( |
| Value::fromI64(static_cast<uint64_t>(lhs.asI64()) % static_cast<uint64_t>(checkConstantDivision<uint64_t>(lhs, rhs).asI64())) |
| ), |
| BLOCK( |
| emitModOrDiv<uint64_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ), |
| BLOCK( |
| emitModOrDiv<uint64_t, true>(lhs, lhsLocation, rhs, rhsLocation, result, resultLocation); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Div(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Div", TypeKind::F32, |
| BLOCK(Value::fromF32(lhs.asF32() / rhs.asF32())), |
| BLOCK( |
| m_jit.divFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.divFloat(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Div(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Div", TypeKind::F64, |
| BLOCK(Value::fromF64(lhs.asF64() / rhs.asF64())), |
| BLOCK( |
| m_jit.divDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.divDouble(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| template<MinOrMax IsMinOrMax, typename FloatType> |
| void BBQJIT::emitFloatingPointMinOrMax(FPRReg left, FPRReg right, FPRReg result) |
| { |
| constexpr bool is32 = sizeof(FloatType) == 4; |
| |
| #if CPU(ARM64) |
| if constexpr (is32 && IsMinOrMax == MinOrMax::Min) |
| m_jit.floatMin(left, right, result); |
| else if constexpr (is32) |
| m_jit.floatMax(left, right, result); |
| else if constexpr (IsMinOrMax == MinOrMax::Min) |
| m_jit.doubleMin(left, right, result); |
| else |
| m_jit.doubleMax(left, right, result); |
| return; |
| #else |
| // Entry |
| Jump isEqual = is32 |
| ? m_jit.branchFloat(MacroAssembler::DoubleEqualAndOrdered, left, right) |
| : m_jit.branchDouble(MacroAssembler::DoubleEqualAndOrdered, left, right); |
| |
| // Left is not equal to right |
| Jump isLessThan = is32 |
| ? m_jit.branchFloat(MacroAssembler::DoubleLessThanAndOrdered, left, right) |
| : m_jit.branchDouble(MacroAssembler::DoubleLessThanAndOrdered, left, right); |
| |
| // Left is not less than right |
| Jump isGreaterThan = is32 |
| ? m_jit.branchFloat(MacroAssembler::DoubleGreaterThanAndOrdered, left, right) |
| : m_jit.branchDouble(MacroAssembler::DoubleGreaterThanAndOrdered, left, right); |
| |
| // NaN |
| if constexpr (is32) |
| m_jit.addFloat(left, right, result); |
| else |
| m_jit.addDouble(left, right, result); |
| Jump afterNaN = m_jit.jump(); |
| |
| // Left is strictly greater than right |
| isGreaterThan.link(&m_jit); |
| auto isGreaterThanResult = IsMinOrMax == MinOrMax::Max ? left : right; |
| m_jit.moveDouble(isGreaterThanResult, result); |
| Jump afterGreaterThan = m_jit.jump(); |
| |
| // Left is strictly less than right |
| isLessThan.link(&m_jit); |
| auto isLessThanResult = IsMinOrMax == MinOrMax::Max ? right : left; |
| m_jit.moveDouble(isLessThanResult, result); |
| Jump afterLessThan = m_jit.jump(); |
| |
| // Left is equal to right |
| isEqual.link(&m_jit); |
| if constexpr (is32 && IsMinOrMax == MinOrMax::Max) |
| m_jit.andFloat(left, right, result); |
| else if constexpr (is32) |
| m_jit.orFloat(left, right, result); |
| else if constexpr (IsMinOrMax == MinOrMax::Max) |
| m_jit.andDouble(left, right, result); |
| else |
| m_jit.orDouble(left, right, result); |
| |
| // Continuation |
| afterNaN.link(&m_jit); |
| afterGreaterThan.link(&m_jit); |
| afterLessThan.link(&m_jit); |
| #endif |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Min(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Min", TypeKind::F32, |
| BLOCK(Value::fromF32(computeFloatingPointMinOrMax<MinOrMax::Min>(lhs.asF32(), rhs.asF32()))), |
| BLOCK( |
| emitFloatingPointMinOrMax<MinOrMax::Min, float>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| emitFloatingPointMinOrMax<MinOrMax::Min, float>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Min(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Min", TypeKind::F64, |
| BLOCK(Value::fromF64(computeFloatingPointMinOrMax<MinOrMax::Min>(lhs.asF64(), rhs.asF64()))), |
| BLOCK( |
| emitFloatingPointMinOrMax<MinOrMax::Min, double>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| emitFloatingPointMinOrMax<MinOrMax::Min, double>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Max(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Max", TypeKind::F32, |
| BLOCK(Value::fromF32(computeFloatingPointMinOrMax<MinOrMax::Max>(lhs.asF32(), rhs.asF32()))), |
| BLOCK( |
| emitFloatingPointMinOrMax<MinOrMax::Max, float>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| emitFloatingPointMinOrMax<MinOrMax::Max, float>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Max(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Max", TypeKind::F64, |
| BLOCK(Value::fromF64(computeFloatingPointMinOrMax<MinOrMax::Max>(lhs.asF64(), rhs.asF64()))), |
| BLOCK( |
| emitFloatingPointMinOrMax<MinOrMax::Max, double>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| emitFloatingPointMinOrMax<MinOrMax::Max, double>(lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32And(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32And", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() & rhs.asI32())), |
| BLOCK( |
| m_jit.and32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| m_jit.and32(Imm32(ImmHelpers::imm(lhs, rhs).asI32()), ImmHelpers::regLocation(lhsLocation, rhsLocation).asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Xor(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32Xor", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() ^ rhs.asI32())), |
| BLOCK( |
| m_jit.xor32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| m_jit.xor32(Imm32(ImmHelpers::imm(lhs, rhs).asI32()), ImmHelpers::regLocation(lhsLocation, rhsLocation).asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Or(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "I32Or", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() | rhs.asI32())), |
| BLOCK( |
| m_jit.or32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| m_jit.or32(Imm32(ImmHelpers::imm(lhs, rhs).asI32()), ImmHelpers::regLocation(lhsLocation, rhsLocation).asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| void BBQJIT::moveShiftAmountIfNecessary(Location& rhsLocation) |
| { |
| if constexpr (isX86()) { |
| m_jit.move(rhsLocation.asGPR(), shiftRCX); |
| rhsLocation = Location::fromGPR(shiftRCX); |
| } |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Shl(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_SHIFT; |
| EMIT_BINARY( |
| "I32Shl", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() << rhs.asI32())), |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.lshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.lshift32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| emitMoveConst(lhs, lhsLocation = Location::fromGPR(wasmScratchGPR)); |
| m_jit.lshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32ShrS(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_SHIFT; |
| EMIT_BINARY( |
| "I32ShrS", TypeKind::I32, |
| BLOCK(Value::fromI32(lhs.asI32() >> rhs.asI32())), |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.rshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.rshift32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| emitMoveConst(lhs, lhsLocation = Location::fromGPR(wasmScratchGPR)); |
| m_jit.rshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32ShrU(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_SHIFT; |
| EMIT_BINARY( |
| "I32ShrU", TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<uint32_t>(lhs.asI32()) >> static_cast<uint32_t>(rhs.asI32()))), |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.urshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.urshift32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| emitMoveConst(lhs, lhsLocation = Location::fromGPR(wasmScratchGPR)); |
| m_jit.urshift32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Rotl(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_SHIFT; |
| EMIT_BINARY( |
| "I32Rotl", TypeKind::I32, |
| BLOCK(Value::fromI32(B3::rotateLeft(lhs.asI32(), rhs.asI32()))), |
| #if CPU(X86_64) |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.rotateLeft32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.rotateLeft32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| emitMoveConst(lhs, resultLocation); |
| m_jit.rotateLeft32(resultLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| #else |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.neg32(rhsLocation.asGPR(), wasmScratchGPR); |
| m_jit.rotateRight32(lhsLocation.asGPR(), wasmScratchGPR, resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.rotateRight32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(-rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.neg32(rhsLocation.asGPR(), wasmScratchGPR); |
| emitMoveConst(lhs, resultLocation); |
| m_jit.rotateRight32(resultLocation.asGPR(), wasmScratchGPR, resultLocation.asGPR()); |
| } |
| ) |
| #endif |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Rotr(Value lhs, Value rhs, Value& result) |
| { |
| PREPARE_FOR_SHIFT; |
| EMIT_BINARY( |
| "I32Rotr", TypeKind::I32, |
| BLOCK(Value::fromI32(B3::rotateRight(lhs.asI32(), rhs.asI32()))), |
| BLOCK( |
| moveShiftAmountIfNecessary(rhsLocation); |
| m_jit.rotateRight32(lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.rotateRight32(lhsLocation.asGPR(), m_jit.trustedImm32ForShift(Imm32(rhs.asI32())), resultLocation.asGPR()); |
| else { |
| moveShiftAmountIfNecessary(rhsLocation); |
| emitMoveConst(lhs, Location::fromGPR(wasmScratchGPR)); |
| m_jit.rotateRight32(wasmScratchGPR, rhsLocation.asGPR(), resultLocation.asGPR()); |
| } |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Clz(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Clz", TypeKind::I32, |
| BLOCK(Value::fromI32(WTF::clz(operand.asI32()))), |
| BLOCK( |
| m_jit.countLeadingZeros32(operandLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Ctz(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Ctz", TypeKind::I32, |
| BLOCK(Value::fromI32(WTF::ctz(operand.asI32()))), |
| BLOCK( |
| m_jit.countTrailingZeros32(operandLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ); |
| } |
| |
| PartialResult BBQJIT::emitCompareI32(const char* opcode, Value& lhs, Value& rhs, Value& result, RelationalCondition condition, bool (*comparator)(int32_t lhs, int32_t rhs)) |
| { |
| EMIT_BINARY( |
| opcode, TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<int32_t>(comparator(lhs.asI32(), rhs.asI32())))), |
| BLOCK( |
| m_jit.compare32(condition, lhsLocation.asGPR(), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| if (rhs.isConst()) |
| m_jit.compare32(condition, lhsLocation.asGPR(), Imm32(rhs.asI32()), resultLocation.asGPR()); |
| else |
| m_jit.compare32(condition, Imm32(lhs.asI32()), rhsLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| #define RELOP_AS_LAMBDA(op) [](auto lhs, auto rhs) -> auto { return lhs op rhs; } |
| #define TYPED_RELOP_AS_LAMBDA(type, op) [](auto lhs, auto rhs) -> auto { return static_cast<type>(lhs) op static_cast<type>(rhs); } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Eq(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32Eq", lhs, rhs, result, RelationalCondition::Equal, RELOP_AS_LAMBDA( == )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64Eq(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64Eq", lhs, rhs, result, RelationalCondition::Equal, RELOP_AS_LAMBDA( == )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Ne(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32Ne", lhs, rhs, result, RelationalCondition::NotEqual, RELOP_AS_LAMBDA( != )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64Ne(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64Ne", lhs, rhs, result, RelationalCondition::NotEqual, RELOP_AS_LAMBDA( != )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32LtS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32LtS", lhs, rhs, result, RelationalCondition::LessThan, RELOP_AS_LAMBDA( < )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64LtS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64LtS", lhs, rhs, result, RelationalCondition::LessThan, RELOP_AS_LAMBDA( < )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32LeS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32LeS", lhs, rhs, result, RelationalCondition::LessThanOrEqual, RELOP_AS_LAMBDA( <= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64LeS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64LeS", lhs, rhs, result, RelationalCondition::LessThanOrEqual, RELOP_AS_LAMBDA( <= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32GtS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32GtS", lhs, rhs, result, RelationalCondition::GreaterThan, RELOP_AS_LAMBDA( > )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64GtS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64GtS", lhs, rhs, result, RelationalCondition::GreaterThan, RELOP_AS_LAMBDA( > )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32GeS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32GeS", lhs, rhs, result, RelationalCondition::GreaterThanOrEqual, RELOP_AS_LAMBDA( >= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64GeS(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64GeS", lhs, rhs, result, RelationalCondition::GreaterThanOrEqual, RELOP_AS_LAMBDA( >= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32LtU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32LtU", lhs, rhs, result, RelationalCondition::Below, TYPED_RELOP_AS_LAMBDA(uint32_t, <)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64LtU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64LtU", lhs, rhs, result, RelationalCondition::Below, TYPED_RELOP_AS_LAMBDA(uint64_t, <)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32LeU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32LeU", lhs, rhs, result, RelationalCondition::BelowOrEqual, TYPED_RELOP_AS_LAMBDA(uint32_t, <=)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64LeU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64LeU", lhs, rhs, result, RelationalCondition::BelowOrEqual, TYPED_RELOP_AS_LAMBDA(uint64_t, <=)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32GtU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32GtU", lhs, rhs, result, RelationalCondition::Above, TYPED_RELOP_AS_LAMBDA(uint32_t, >)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64GtU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64GtU", lhs, rhs, result, RelationalCondition::Above, TYPED_RELOP_AS_LAMBDA(uint64_t, >)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32GeU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI32("I32GeU", lhs, rhs, result, RelationalCondition::AboveOrEqual, TYPED_RELOP_AS_LAMBDA(uint32_t, >=)); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64GeU(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareI64("I64GeU", lhs, rhs, result, RelationalCondition::AboveOrEqual, TYPED_RELOP_AS_LAMBDA(uint64_t, >=)); |
| } |
| |
| PartialResult BBQJIT::emitCompareF32(const char* opcode, Value& lhs, Value& rhs, Value& result, DoubleCondition condition, bool (*comparator)(float lhs, float rhs)) |
| { |
| EMIT_BINARY( |
| opcode, TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<int32_t>(comparator(lhs.asF32(), rhs.asF32())))), |
| BLOCK( |
| m_jit.compareFloat(condition, lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.compareFloat(condition, lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| PartialResult BBQJIT::emitCompareF64(const char* opcode, Value& lhs, Value& rhs, Value& result, DoubleCondition condition, bool (*comparator)(double lhs, double rhs)) |
| { |
| EMIT_BINARY( |
| opcode, TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<int32_t>(comparator(lhs.asF64(), rhs.asF64())))), |
| BLOCK( |
| m_jit.compareDouble(condition, lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asGPR()); |
| ), |
| BLOCK( |
| ImmHelpers::immLocation(lhsLocation, rhsLocation) = Location::fromFPR(wasmScratchFPR); |
| emitMoveConst(ImmHelpers::imm(lhs, rhs), Location::fromFPR(wasmScratchFPR)); |
| m_jit.compareDouble(condition, lhsLocation.asFPR(), rhsLocation.asFPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Eq(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Eq", lhs, rhs, result, DoubleCondition::DoubleEqualAndOrdered, RELOP_AS_LAMBDA( == )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Eq(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Eq", lhs, rhs, result, DoubleCondition::DoubleEqualAndOrdered, RELOP_AS_LAMBDA( == )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Ne(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Ne", lhs, rhs, result, DoubleCondition::DoubleNotEqualOrUnordered, RELOP_AS_LAMBDA( != )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Ne(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Ne", lhs, rhs, result, DoubleCondition::DoubleNotEqualOrUnordered, RELOP_AS_LAMBDA( != )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Lt(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Lt", lhs, rhs, result, DoubleCondition::DoubleLessThanAndOrdered, RELOP_AS_LAMBDA( < )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Lt(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Lt", lhs, rhs, result, DoubleCondition::DoubleLessThanAndOrdered, RELOP_AS_LAMBDA( < )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Le(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Le", lhs, rhs, result, DoubleCondition::DoubleLessThanOrEqualAndOrdered, RELOP_AS_LAMBDA( <= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Le(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Le", lhs, rhs, result, DoubleCondition::DoubleLessThanOrEqualAndOrdered, RELOP_AS_LAMBDA( <= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Gt(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Gt", lhs, rhs, result, DoubleCondition::DoubleGreaterThanAndOrdered, RELOP_AS_LAMBDA( > )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Gt(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Gt", lhs, rhs, result, DoubleCondition::DoubleGreaterThanAndOrdered, RELOP_AS_LAMBDA( > )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Ge(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF32("F32Ge", lhs, rhs, result, DoubleCondition::DoubleGreaterThanOrEqualAndOrdered, RELOP_AS_LAMBDA( >= )); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Ge(Value lhs, Value rhs, Value& result) |
| { |
| return emitCompareF64("F64Ge", lhs, rhs, result, DoubleCondition::DoubleGreaterThanOrEqualAndOrdered, RELOP_AS_LAMBDA( >= )); |
| } |
| |
| #undef RELOP_AS_LAMBDA |
| #undef TYPED_RELOP_AS_LAMBDA |
| |
| PartialResult BBQJIT::addI32Extend8S(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Extend8S", TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<int32_t>(static_cast<int8_t>(operand.asI32())))), |
| BLOCK( |
| m_jit.signExtend8To32(operandLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Extend16S(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Extend16S", TypeKind::I32, |
| BLOCK(Value::fromI32(static_cast<int32_t>(static_cast<int16_t>(operand.asI32())))), |
| BLOCK( |
| m_jit.signExtend16To32(operandLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Eqz(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Eqz", TypeKind::I32, |
| BLOCK(Value::fromI32(!operand.asI32())), |
| BLOCK( |
| m_jit.test32(ResultCondition::Zero, operandLocation.asGPR(), operandLocation.asGPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32Popcnt(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32Popcnt", TypeKind::I32, |
| BLOCK(Value::fromI32(std::popcount(static_cast<uint32_t>(operand.asI32())))), |
| BLOCK( |
| if (m_jit.supportsCountPopulation()) |
| m_jit.countPopulation32(operandLocation.asGPR(), resultLocation.asGPR(), wasmScratchFPR); |
| else { |
| // The EMIT_UNARY(...) macro will already assign result to the top value on the stack and give it a register, |
| // so it should be able to be passed in. However, this creates a somewhat nasty tacit dependency - emitCCall |
| // will bind result to the returnValueGPR, which will error if result is already bound to a different register |
| // (to avoid mucking with the register allocator state). It shouldn't currently error specifically because we |
| // only allocate caller-saved registers, which get flushed across the call regardless; however, if we add |
| // callee-saved register allocation to BBQJIT in the future, we could get very niche errors. |
| // |
| // We avoid this by consuming the result before passing it to emitCCall, which also saves us the mov for spilling. |
| consume(result); |
| auto arg = Value::pinned(TypeKind::I32, operandLocation); |
| emitCCall(&operationPopcount32, singleElementSpan(arg), result); |
| } |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64Popcnt(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I64Popcnt", TypeKind::I64, |
| BLOCK(Value::fromI64(std::popcount(static_cast<uint64_t>(operand.asI64())))), |
| BLOCK( |
| if (m_jit.supportsCountPopulation()) |
| m_jit.countPopulation64(operandLocation.asGPR(), resultLocation.asGPR(), wasmScratchFPR); |
| else { |
| // The EMIT_UNARY(...) macro will already assign result to the top value on the stack and give it a register, |
| // so it should be able to be passed in. However, this creates a somewhat nasty tacit dependency - emitCCall |
| // will bind result to the returnValueGPR, which will error if result is already bound to a different register |
| // (to avoid mucking with the register allocator state). It shouldn't currently error specifically because we |
| // only allocate caller-saved registers, which get flushed across the call regardless; however, if we add |
| // callee-saved register allocation to BBQJIT in the future, we could get very niche errors. |
| // |
| // We avoid this by consuming the result before passing it to emitCCall, which also saves us the mov for spilling. |
| consume(result); |
| auto arg = Value::pinned(TypeKind::I64, operandLocation); |
| emitCCall(&operationPopcount64, singleElementSpan(arg), result); |
| } |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32ReinterpretF32(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "I32ReinterpretF32", TypeKind::I32, |
| BLOCK(Value::fromI32(std::bit_cast<int32_t>(operand.asF32()))), |
| BLOCK( |
| m_jit.moveFloatTo32(operandLocation.asFPR(), resultLocation.asGPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32ReinterpretI32(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32ReinterpretI32", TypeKind::F32, |
| BLOCK(Value::fromF32(std::bit_cast<float>(operand.asI32()))), |
| BLOCK( |
| m_jit.move32ToFloat(operandLocation.asGPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32DemoteF64(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32DemoteF64", TypeKind::F32, |
| BLOCK(Value::fromF32(operand.asF64())), |
| BLOCK( |
| m_jit.convertDoubleToFloat(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64PromoteF32(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F64PromoteF32", TypeKind::F64, |
| BLOCK(Value::fromF64(operand.asF32())), |
| BLOCK( |
| m_jit.convertFloatToDouble(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Copysign(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F64Copysign", TypeKind::F64, |
| BLOCK(Value::fromF64(doubleCopySign(lhs.asF64(), rhs.asF64()))), |
| BLOCK( |
| m_jit.move64ToDouble(TrustedImm64(std::numeric_limits<int64_t>::min()), wasmScratchFPR); |
| m_jit.andDouble(rhsLocation.asFPR(), wasmScratchFPR, wasmScratchFPR); |
| m_jit.absDouble(lhsLocation.asFPR(), resultLocation.asFPR()); |
| m_jit.orDouble(wasmScratchFPR, resultLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| if (lhs.isConst()) { |
| m_jit.move64ToDouble(TrustedImm64(std::numeric_limits<int64_t>::min()), wasmScratchFPR); |
| m_jit.andDouble(rhsLocation.asFPR(), wasmScratchFPR, wasmScratchFPR); |
| emitMoveConst(Value::fromF64(std::abs(lhs.asF64())), resultLocation); |
| m_jit.orDouble(resultLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| } else { |
| bool signBit = std::bit_cast<uint64_t>(rhs.asF64()) & 0x8000000000000000ull; |
| m_jit.absDouble(lhsLocation.asFPR(), resultLocation.asFPR()); |
| if (signBit) |
| m_jit.negateDouble(resultLocation.asFPR(), resultLocation.asFPR()); |
| } |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32ConvertSI32(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32ConvertSI32", TypeKind::F32, |
| BLOCK(Value::fromF32(operand.asI32())), |
| BLOCK( |
| m_jit.convertInt32ToFloat(operandLocation.asGPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64ConvertSI32(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F64ConvertSI32", TypeKind::F64, |
| BLOCK(Value::fromF64(operand.asI32())), |
| BLOCK( |
| m_jit.convertInt32ToDouble(operandLocation.asGPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Copysign(Value lhs, Value rhs, Value& result) |
| { |
| EMIT_BINARY( |
| "F32Copysign", TypeKind::F32, |
| BLOCK(Value::fromF32(floatCopySign(lhs.asF32(), rhs.asF32()))), |
| BLOCK( |
| m_jit.move32ToFloat(TrustedImm32(std::numeric_limits<int32_t>::min()), wasmScratchFPR); |
| m_jit.andFloat(rhsLocation.asFPR(), wasmScratchFPR, wasmScratchFPR); |
| m_jit.absFloat(lhsLocation.asFPR(), resultLocation.asFPR()); |
| m_jit.orFloat(wasmScratchFPR, resultLocation.asFPR(), resultLocation.asFPR()); |
| ), |
| BLOCK( |
| if (lhs.isConst()) { |
| m_jit.move32ToFloat(TrustedImm32(std::numeric_limits<int32_t>::min()), wasmScratchFPR); |
| m_jit.andFloat(rhsLocation.asFPR(), wasmScratchFPR, wasmScratchFPR); |
| emitMoveConst(Value::fromF32(std::abs(lhs.asF32())), resultLocation); |
| m_jit.orFloat(resultLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| } else { |
| bool signBit = std::bit_cast<uint32_t>(rhs.asF32()) & 0x80000000u; |
| m_jit.absFloat(lhsLocation.asFPR(), resultLocation.asFPR()); |
| if (signBit) { |
| #if CPU(X86_64) |
| m_jit.moveFloatTo32(resultLocation.asFPR(), wasmScratchGPR); |
| m_jit.xor32(TrustedImm32(std::bit_cast<uint32_t>(static_cast<float>(-0.0))), wasmScratchGPR); |
| m_jit.move32ToFloat(wasmScratchGPR, resultLocation.asFPR()); |
| #else |
| m_jit.negateFloat(resultLocation.asFPR(), resultLocation.asFPR()); |
| #endif |
| } |
| } |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Abs(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32Abs", TypeKind::F32, |
| BLOCK(Value::fromF32(std::abs(operand.asF32()))), |
| BLOCK( |
| #if CPU(X86_64) |
| m_jit.move32ToFloat(TrustedImm32(0x7fffffffll), wasmScratchFPR); |
| m_jit.andFloat(operandLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| #else |
| m_jit.absFloat(operandLocation.asFPR(), resultLocation.asFPR()); |
| #endif |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Abs(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F64Abs", TypeKind::F64, |
| BLOCK(Value::fromF64(std::abs(operand.asF64()))), |
| BLOCK( |
| #if CPU(X86_64) |
| m_jit.move64ToDouble(TrustedImm64(0x7fffffffffffffffll), wasmScratchFPR); |
| m_jit.andDouble(operandLocation.asFPR(), wasmScratchFPR, resultLocation.asFPR()); |
| #else |
| m_jit.absDouble(operandLocation.asFPR(), resultLocation.asFPR()); |
| #endif |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Sqrt(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32Sqrt", TypeKind::F32, |
| BLOCK(Value::fromF32(Math::sqrtFloat(operand.asF32()))), |
| BLOCK( |
| m_jit.sqrtFloat(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Sqrt(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F64Sqrt", TypeKind::F64, |
| BLOCK(Value::fromF64(Math::sqrtDouble(operand.asF64()))), |
| BLOCK( |
| m_jit.sqrtDouble(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF32Neg(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F32Neg", TypeKind::F32, |
| BLOCK(Value::fromF32(-operand.asF32())), |
| BLOCK( |
| m_jit.negateFloat(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addF64Neg(Value operand, Value& result) |
| { |
| EMIT_UNARY( |
| "F64Neg", TypeKind::F64, |
| BLOCK(Value::fromF64(-operand.asF64())), |
| BLOCK( |
| m_jit.negateDouble(operandLocation.asFPR(), resultLocation.asFPR()); |
| ) |
| ) |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32TruncSF32(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I32TruncSF32, operand, result, Types::I32, Types::F32); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32TruncSF64(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I32TruncSF64, operand, result, Types::I32, Types::F64); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32TruncUF32(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I32TruncUF32, operand, result, Types::I32, Types::F32); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI32TruncUF64(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I32TruncUF64, operand, result, Types::I32, Types::F64); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64TruncSF32(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I64TruncSF32, operand, result, Types::I64, Types::F32); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64TruncSF64(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I64TruncSF64, operand, result, Types::I64, Types::F64); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64TruncUF32(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I64TruncUF32, operand, result, Types::I64, Types::F32); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addI64TruncUF64(Value operand, Value& result) |
| { |
| return truncTrapping(OpType::I64TruncUF64, operand, result, Types::I64, Types::F64); |
| } |
| |
| // References |
| |
| [[nodiscard]] PartialResult BBQJIT::addRefEq(Value ref0, Value ref1, Value& result) |
| { |
| return addI64Eq(ref0, ref1, result); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addRefFunc(FunctionSpaceIndex index, Value& result) |
| { |
| // FIXME: Emit this inline <https://bugs.webkit.org/show_bug.cgi?id=198506>. |
| TypeKind returnType = TypeKind::Ref; |
| |
| Vector<Value, 8> arguments = { |
| instanceValue(), |
| Value::fromI32(index) |
| }; |
| result = topValue(returnType); |
| emitCCall(&operationWasmRefFunc, arguments, result); |
| |
| return { }; |
| } |
| |
| void BBQJIT::emitEntryTierUpCheck() |
| { |
| if (!canTierUpToOMG()) |
| return; |
| |
| #if ENABLE(WEBASSEMBLY_OMGJIT) |
| static_assert(GPRInfo::nonPreservedNonArgumentGPR0 == wasmScratchGPR); |
| m_jit.move(TrustedImmPtr(std::bit_cast<uintptr_t>(&m_callee.tierUpCounter().m_counter)), wasmScratchGPR); |
| Jump tierUp = m_jit.branchAdd32(CCallHelpers::PositiveOrZero, TrustedImm32(TierUpCount::functionEntryIncrement()), Address(wasmScratchGPR)); |
| MacroAssembler::Label tierUpResume = m_jit.label(); |
| addLatePath(origin(), [tierUp, tierUpResume](BBQJIT& generator, CCallHelpers& jit) { |
| tierUp.link(&jit); |
| jit.move(GPRInfo::callFrameRegister, GPRInfo::nonPreservedNonArgumentGPR0); |
| jit.nearCallThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(triggerOMGEntryTierUpThunkGenerator(generator.m_usesSIMD)).code())); |
| jit.jump(tierUpResume); |
| }); |
| #else |
| RELEASE_ASSERT_NOT_REACHED(); |
| #endif |
| } |
| |
| // Control flow |
| [[nodiscard]] ControlData BBQJIT::addTopLevel(BlockSignature&& signature) |
| { |
| if (Options::verboseBBQJITInstructions()) [[unlikely]] { |
| auto nameSection = m_info.nameSection; |
| std::pair<const Name*, RefPtr<NameSection>> name = nameSection->get(m_functionIndex); |
| dataLog("BBQ\tFunction "); |
| if (name.first) |
| dataLog(makeString(*name.first)); |
| else |
| dataLog(m_functionIndex); |
| dataLogLn(" ", m_functionSignature.get()); |
| LOG_INDENT(); |
| } |
| |
| m_pcToCodeOriginMapBuilder.appendItem(m_jit.label(), PCToCodeOriginMapBuilder::defaultCodeOrigin()); |
| m_jit.emitFunctionPrologue(); |
| emitPushCalleeSaves(); |
| m_topLevel = ControlData(*this, BlockType::TopLevel, WTF::move(signature), 0); |
| |
| JIT_COMMENT(m_jit, "Store boxed JIT callee"); |
| m_jit.move(CCallHelpers::TrustedImmPtr(CalleeBits::boxNativeCallee(&m_callee)), wasmScratchGPR); |
| static_assert(CallFrameSlot::codeBlock + 1 == CallFrameSlot::callee); |
| if constexpr (is32Bit()) { |
| CCallHelpers::Address calleeSlot { GPRInfo::callFrameRegister, CallFrameSlot::callee * sizeof(Register) }; |
| m_jit.storePtr(wasmScratchGPR, calleeSlot.withOffset(PayloadOffset)); |
| m_jit.store32(CCallHelpers::TrustedImm32(JSValue::NativeCalleeTag), calleeSlot.withOffset(TagOffset)); |
| m_jit.storePtr(GPRInfo::wasmContextInstancePointer, CCallHelpers::addressFor(CallFrameSlot::codeBlock)); |
| } else |
| m_jit.storePairPtr(GPRInfo::wasmContextInstancePointer, wasmScratchGPR, GPRInfo::callFrameRegister, CCallHelpers::TrustedImm32(CallFrameSlot::codeBlock * sizeof(Register))); |
| |
| if (m_profiledCallee.hasExceptionHandlers()) |
| m_jit.store32(CCallHelpers::TrustedImm32(wasmInvalidCallSiteIndex), CCallHelpers::tagFor(CallFrameSlot::argumentCountIncludingThis)); |
| |
| // Because we compile in a single pass, we always need to pessimistically check for stack underflow/overflow. |
| static_assert(wasmScratchGPR == GPRInfo::nonPreservedNonArgumentGPR0); |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(m_frameSize), wasmScratchGPR); |
| |
| MacroAssembler::JumpList overflow; |
| JIT_COMMENT(m_jit, "Stack overflow check"); |
| #if !CPU(ADDRESS64) |
| overflow.append(m_jit.branchPtr(CCallHelpers::Above, wasmScratchGPR, GPRInfo::callFrameRegister)); |
| #endif |
| overflow.append(m_jit.branchPtr(CCallHelpers::LessThan, wasmScratchGPR, CCallHelpers::Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfSoftStackLimit()))); |
| overflow.linkThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(throwStackOverflowFromWasmThunkGenerator).code()), &m_jit); |
| |
| m_jit.move(wasmScratchGPR, MacroAssembler::stackPointerRegister); |
| |
| Address baselineDataAddress(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfBaselineData(m_info, m_functionIndex)); |
| m_jit.loadPtr(baselineDataAddress, GPRInfo::jitDataRegister); |
| Jump materialize = m_jit.branchTestPtr(CCallHelpers::Zero, GPRInfo::jitDataRegister); |
| MacroAssembler::Label done = m_jit.label(); |
| addLatePath(origin(), [materialize = WTF::move(materialize), done, baselineDataAddress](BBQJIT&, CCallHelpers& jit) { |
| materialize.link(&jit); |
| jit.move(GPRInfo::callFrameRegister, GPRInfo::nonPreservedNonArgumentGPR0); |
| jit.nearCallThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(materializeBaselineDataGenerator).code())); |
| jit.loadPtr(baselineDataAddress, GPRInfo::jitDataRegister); |
| jit.jump(done); |
| }); |
| m_jit.add32(TrustedImm32(1), CCallHelpers::Address(GPRInfo::jitDataRegister, BaselineData::offsetOfTotalCount())); |
| |
| LocalOrTempIndex i = 0; |
| for (; i < m_arguments.size(); ++i) |
| flushValue(Value::fromLocal(m_parser->typeOfLocal(i).kind, i)); |
| |
| // Zero all locals that aren't initialized by arguments. |
| enum class ClearMode { Zero, JSNull }; |
| std::optional<int32_t> highest; |
| std::optional<int32_t> lowest; |
| auto flushZeroClear = [&]() { |
| if (!lowest) |
| return; |
| size_t size = highest.value() - lowest.value(); |
| int32_t pointer = lowest.value(); |
| |
| // Adjust pointer offset to be efficient for paired-stores. |
| if (pointer & 4 && size >= 4) { |
| m_jit.store32(TrustedImm32(0), Address(GPRInfo::callFrameRegister, pointer)); |
| pointer += 4; |
| size -= 4; |
| } |
| |
| #if CPU(ARM64) |
| if (pointer & 8 && size >= 8) { |
| m_jit.store64(TrustedImm64(0), Address(GPRInfo::callFrameRegister, pointer)); |
| pointer += 8; |
| size -= 8; |
| } |
| |
| unsigned count = size / 16; |
| for (unsigned i = 0; i < count; ++i) { |
| m_jit.storePair64(ARM64Registers::zr, ARM64Registers::zr, GPRInfo::callFrameRegister, TrustedImm32(pointer)); |
| pointer += 16; |
| size -= 16; |
| } |
| if (size & 8) { |
| m_jit.store64(TrustedImm64(0), Address(GPRInfo::callFrameRegister, pointer)); |
| pointer += 8; |
| size -= 8; |
| } |
| #else |
| unsigned count = size / 8; |
| for (unsigned i = 0; i < count; ++i) { |
| m_jit.store32(TrustedImm32(0), Address(GPRInfo::callFrameRegister, pointer)); |
| m_jit.store32(TrustedImm32(0), Address(GPRInfo::callFrameRegister, pointer+4)); |
| pointer += 8; |
| size -= 8; |
| } |
| #endif |
| |
| if (size & 4) { |
| m_jit.store32(TrustedImm32(0), Address(GPRInfo::callFrameRegister, pointer)); |
| pointer += 4; |
| size -= 4; |
| } |
| ASSERT(size == 0); |
| |
| highest = std::nullopt; |
| lowest = std::nullopt; |
| }; |
| |
| auto clear = [&](ClearMode mode, TypeKind type, Location location) { |
| if (mode == ClearMode::JSNull) { |
| flushZeroClear(); |
| emitStoreConst(Value::fromI64(std::bit_cast<uint64_t>(JSValue::encode(jsNull()))), location); |
| return; |
| } |
| if (!highest) |
| highest = location.asStackOffset() + sizeOfType(type); |
| lowest = location.asStackOffset(); |
| }; |
| |
| JIT_COMMENT(m_jit, "initialize locals"); |
| for (; i < m_locals.size(); ++i) { |
| TypeKind type = m_parser->typeOfLocal(i).kind; |
| switch (type) { |
| case TypeKind::I32: |
| case TypeKind::I31ref: |
| case TypeKind::F32: |
| case TypeKind::F64: |
| case TypeKind::I64: |
| case TypeKind::Struct: |
| case TypeKind::Rec: |
| case TypeKind::Func: |
| case TypeKind::Array: |
| case TypeKind::Sub: |
| case TypeKind::Subfinal: |
| case TypeKind::V128: |
| clear(ClearMode::Zero, type, m_locals[i]); |
| break; |
| case TypeKind::Exnref: |
| case TypeKind::Externref: |
| case TypeKind::Funcref: |
| case TypeKind::Ref: |
| case TypeKind::RefNull: |
| case TypeKind::Structref: |
| case TypeKind::Arrayref: |
| case TypeKind::Eqref: |
| case TypeKind::Anyref: |
| case TypeKind::Noexnref: |
| case TypeKind::Noneref: |
| case TypeKind::Nofuncref: |
| case TypeKind::Noexternref: |
| clear(ClearMode::JSNull, type, m_locals[i]); |
| break; |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| } |
| flushZeroClear(); |
| JIT_COMMENT(m_jit, "initialize locals done"); |
| |
| for (size_t i = 0; i < m_functionSignature->argumentCount(); i ++) |
| m_topLevel.touch(i); // Ensure arguments are flushed to persistent locations when this block ends. |
| |
| emitEntryTierUpCheck(); |
| |
| return m_topLevel; |
| } |
| |
| bool BBQJIT::hasLoops() const |
| { |
| return m_compilation->bbqLoopEntrypoints.size(); |
| } |
| |
| MacroAssembler::Label BBQJIT::addLoopOSREntrypoint() |
| { |
| // To handle OSR entry into loops, we emit a second entrypoint, which sets up our call frame then calls an |
| // operation to get the address of the loop we're trying to enter. Unlike the normal prologue, by the time |
| // we emit this entry point, we: |
| // - Know the frame size, so we don't need to patch the constant. |
| // - Can omit the entry tier-up check, since this entry point is only reached when we initially tier up into a loop. |
| // - Don't need to zero our locals, since they are restored from the OSR entry scratch buffer anyway. |
| auto label = m_jit.label(); |
| m_jit.emitFunctionPrologue(); |
| emitPushCalleeSaves(); |
| |
| m_jit.move(CCallHelpers::TrustedImmPtr(CalleeBits::boxNativeCallee(&m_callee)), wasmScratchGPR); |
| static_assert(CallFrameSlot::codeBlock + 1 == CallFrameSlot::callee); |
| if constexpr (is32Bit()) { |
| CCallHelpers::Address calleeSlot { GPRInfo::callFrameRegister, CallFrameSlot::callee * sizeof(Register) }; |
| m_jit.storePtr(wasmScratchGPR, calleeSlot.withOffset(PayloadOffset)); |
| m_jit.store32(CCallHelpers::TrustedImm32(JSValue::NativeCalleeTag), calleeSlot.withOffset(TagOffset)); |
| m_jit.storePtr(GPRInfo::wasmContextInstancePointer, CCallHelpers::addressFor(CallFrameSlot::codeBlock)); |
| } else |
| m_jit.storePairPtr(GPRInfo::wasmContextInstancePointer, wasmScratchGPR, GPRInfo::callFrameRegister, CCallHelpers::TrustedImm32(CallFrameSlot::codeBlock * sizeof(Register))); |
| |
| // Because tiering up code materializes BaselineData, this is always non nullptr. |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfBaselineData(m_info, m_functionIndex)), GPRInfo::jitDataRegister); |
| |
| uint32_t roundedFrameSize = stackCheckSize(); |
| #if CPU(X86_64) || CPU(ARM64) |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(roundedFrameSize), MacroAssembler::stackPointerRegister); |
| #else |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(roundedFrameSize), wasmScratchGPR); |
| m_jit.move(wasmScratchGPR, MacroAssembler::stackPointerRegister); |
| #endif |
| |
| // The loop_osr slow path should have already checked that we have enough space. We have already destroyed the ipint stack, and unwind will see the BBQ catch |
| // since we already replaced callee. So, we just assert that this case doesn't happen to avoid reading a corrupted frame from the bbq catch handler. |
| MacroAssembler::JumpList overflow; |
| #if !CPU(ADDRESS64) |
| overflow.append(m_jit.branchPtr(CCallHelpers::Above, wasmScratchGPR, GPRInfo::callFrameRegister)); |
| overflow.append(m_jit.branchPtr(CCallHelpers::LessThanOrEqual, wasmScratchGPR, CCallHelpers::Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfSoftStackLimit()))); |
| #else |
| overflow.append(m_jit.branchPtr(CCallHelpers::LessThanOrEqual, MacroAssembler::stackPointerRegister, CCallHelpers::Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfSoftStackLimit()))); |
| #endif |
| overflow.linkThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(crashDueToBBQStackOverflowGenerator).code()), &m_jit); |
| |
| // This operation shuffles around values on the stack, until everything is in the right place. Then, |
| // it returns the address of the loop we're jumping to in wasmScratchGPR (so we don't interfere with |
| // anything we just loaded from the scratch buffer into a register) |
| m_jit.probe(tagCFunction<JITProbePtrTag>(operationWasmLoopOSREnterBBQJIT), nullptr); |
| |
| // We expect the loop address to be populated by the probe operation. |
| static_assert(wasmScratchGPR == GPRInfo::nonPreservedNonArgumentGPR0); |
| m_jit.farJump(wasmScratchGPR, WasmEntryPtrTag); |
| return label; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addBlock(BlockSignature&& signature, Stack& enclosingStack, ControlType& result, Stack& newStack) |
| { |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::Block, WTF::move(signature), height); |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("Block", result.signature()); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| result.startBlock(*this, newStack); |
| return { }; |
| } |
| |
| B3::Type BBQJIT::toB3Type(Type type) |
| { |
| return Wasm::toB3Type(type); |
| } |
| |
| B3::Type BBQJIT::toB3Type(TypeKind kind) |
| { |
| switch (kind) { |
| case TypeKind::I32: |
| return B3::Type(B3::Int32); |
| case TypeKind::I64: |
| return B3::Type(B3::Int64); |
| case TypeKind::I31ref: |
| case TypeKind::Ref: |
| case TypeKind::RefNull: |
| case TypeKind::Structref: |
| case TypeKind::Arrayref: |
| case TypeKind::Funcref: |
| case TypeKind::Exnref: |
| case TypeKind::Externref: |
| case TypeKind::Eqref: |
| case TypeKind::Anyref: |
| case TypeKind::Noexnref: |
| case TypeKind::Noneref: |
| case TypeKind::Nofuncref: |
| case TypeKind::Noexternref: |
| return B3::Type(B3::Int64); |
| case TypeKind::F32: |
| return B3::Type(B3::Float); |
| case TypeKind::F64: |
| return B3::Type(B3::Double); |
| case TypeKind::V128: |
| return B3::Type(B3::V128); |
| default: |
| RELEASE_ASSERT_NOT_REACHED(); |
| return B3::Void; |
| } |
| } |
| |
| B3::ValueRep BBQJIT::toB3Rep(Location location) |
| { |
| #if USE(JSVALUE32_64) |
| if (location.isGPR2()) |
| return B3::ValueRep(B3::ValueRep::OSRValueRep, Reg(location.asGPRlo()), Reg(location.asGPRhi())); |
| #endif |
| if (location.isRegister()) |
| return B3::ValueRep(location.isGPR() ? Reg(location.asGPR()) : Reg(location.asFPR())); |
| if (location.isStack()) |
| return B3::ValueRep(ValueLocation::stack(location.asStackOffset())); |
| |
| RELEASE_ASSERT_NOT_REACHED(); |
| return B3::ValueRep(); |
| } |
| |
| // This needs to be kept in sync with WasmIPIntSlowPaths.cpp buildEntryBufferForLoopOSR and OMGIRGenerator::addLoop. |
| StackMap BBQJIT::makeStackMap(const ControlData& data, Stack& enclosingStack) |
| { |
| unsigned numElements = m_locals.size() + data.enclosedHeight() + data.argumentLocations().size(); |
| for (const ControlEntry& entry : m_parser->controlStack()) { |
| if (BBQJIT::ControlData::isTry(entry.controlData)) |
| ++numElements; |
| } |
| |
| StackMap stackMap(numElements); |
| unsigned stackMapIndex = 0; |
| for (unsigned i = 0; i < m_locals.size(); i ++) |
| stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(m_locals[i]), toB3Type(m_localTypes[i])); |
| |
| // Do rethrow slots first because IPInt has them in a shadow stack. |
| for (const ControlEntry& entry : m_parser->controlStack()) { |
| if (ControlData::isAnyCatch(entry.controlData)) { |
| ASSERT(entry.controlData.implicitSlots() == 1); |
| Value exception = this->exception(entry.controlData); |
| stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(exception)), B3::Int64); // Exceptions are EncodedJSValues, so they are always Int64 |
| } else if (BBQJIT::ControlData::isTry(entry.controlData)) { |
| // IPInt reserves rethrow slots based on Try blocks, but there is no exception to rethrow until Catch, |
| // and BBQ and OMG do not represent the implicit exception slot/variable except within the Catch, so |
| // use Void to signify we shouldn't load and constant 0 to zero fill this slot when storing. |
| ASSERT(!entry.controlData.implicitSlots()); |
| stackMap[stackMapIndex++] = OSREntryValue(B3::ValueRep::constant(0), B3::Void); |
| } else |
| ASSERT(!entry.controlData.implicitSlots()); |
| } |
| |
| for (const ControlEntry& entry : m_parser->controlStack()) { |
| for (const TypedExpression& expr : entry.enclosedExpressionStack) |
| stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind)); |
| } |
| |
| for (const TypedExpression& expr : enclosingStack) |
| stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind)); |
| for (unsigned i = 0; i < data.argumentLocations().size(); i++) |
| stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(data.argumentLocations()[i]), toB3Type(data.argumentType(i).kind)); |
| |
| RELEASE_ASSERT(stackMapIndex == numElements); |
| m_osrEntryScratchBufferSize = std::max(m_osrEntryScratchBufferSize, BBQCallee::extraOSRValuesForLoopIndex + numElements); |
| return stackMap; |
| } |
| |
| void BBQJIT::emitLoopTierUpCheckAndOSREntryData(const ControlData& data, Stack& enclosingStack, unsigned loopIndex) |
| { |
| auto& tierUpCounter = m_callee.tierUpCounter(); |
| ASSERT(tierUpCounter.osrEntryTriggers().size() == loopIndex); |
| tierUpCounter.osrEntryTriggers().append(TierUpCount::TriggerReason::DontTrigger); |
| |
| unsigned outerLoops = m_outerLoops.isEmpty() ? UINT32_MAX : m_outerLoops.last(); |
| tierUpCounter.outerLoops().append(outerLoops); |
| m_outerLoops.append(loopIndex); |
| |
| OSREntryData& osrEntryData = tierUpCounter.addOSREntryData(m_functionIndex, loopIndex, makeStackMap(data, enclosingStack)); |
| |
| if (!canTierUpToOMG()) |
| return; |
| |
| #if ENABLE(WEBASSEMBLY_OMGJIT) |
| static_assert(GPRInfo::nonPreservedNonArgumentGPR0 == wasmScratchGPR); |
| m_jit.move(TrustedImmPtr(std::bit_cast<uintptr_t>(&tierUpCounter.m_counter)), wasmScratchGPR); |
| |
| TierUpCount::TriggerReason* forceEntryTrigger = &(tierUpCounter.osrEntryTriggers().last()); |
| static_assert(!static_cast<uint8_t>(TierUpCount::TriggerReason::DontTrigger), "the JIT code assumes non-zero means 'enter'"); |
| static_assert(sizeof(TierUpCount::TriggerReason) == 1, "branchTest8 assumes this size"); |
| |
| Jump forceOSREntry = m_jit.branchTest8(ResultCondition::NonZero, CCallHelpers::AbsoluteAddress(forceEntryTrigger)); |
| Jump tierUp = m_jit.branchAdd32(ResultCondition::PositiveOrZero, TrustedImm32(TierUpCount::loopIncrement()), CCallHelpers::Address(wasmScratchGPR)); |
| MacroAssembler::Label tierUpResume = m_jit.label(); |
| |
| OSREntryData* osrEntryDataPtr = &osrEntryData; |
| |
| addLatePath(origin(), [forceOSREntry, tierUp, tierUpResume, osrEntryDataPtr](BBQJIT&, CCallHelpers& jit) { |
| forceOSREntry.link(&jit); |
| tierUp.link(&jit); |
| |
| jit.probe(tagCFunction<JITProbePtrTag>(operationWasmTriggerOSREntryNow), osrEntryDataPtr); |
| jit.branchTestPtr(CCallHelpers::Zero, GPRInfo::nonPreservedNonArgumentGPR0).linkTo(tierUpResume, &jit); |
| |
| // operationWasmTriggerOSREntryNow is already restoring callee saves. Thus we do not need to restore them before jumping. |
| jit.farJump(GPRInfo::nonPreservedNonArgumentGPR0, WasmEntryPtrTag); |
| }); |
| #else |
| UNUSED_PARAM(osrEntryData); |
| RELEASE_ASSERT_NOT_REACHED(); |
| #endif |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addLoop(BlockSignature&& signature, Stack& enclosingStack, ControlType& result, Stack& newStack, uint32_t loopIndex) |
| { |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::Loop, WTF::move(signature), height); |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("Loop", result.signature()); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| result.startBlock(*this, newStack); |
| result.setLoopLabel(m_jit.label()); |
| |
| RELEASE_ASSERT(m_compilation->bbqLoopEntrypoints.size() == loopIndex); |
| m_compilation->bbqLoopEntrypoints.append(result.loopLabel()); |
| |
| emitLoopTierUpCheckAndOSREntryData(result, enclosingStack, loopIndex); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addIf(Value condition, BlockSignature&& signature, Stack& enclosingStack, ControlData& result, Stack& newStack) |
| { |
| RegisterSet liveScratchGPRs; |
| Location conditionLocation; |
| if (!condition.isConst()) { |
| conditionLocation = loadIfNecessary(condition); |
| liveScratchGPRs.add(conditionLocation.asGPR(), IgnoreVectors); |
| } |
| consume(condition); |
| |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::If, WTF::move(signature), height, liveScratchGPRs); |
| |
| // Despite being conditional, if doesn't need to worry about diverging expression stacks at block boundaries, so it doesn't need multiple exits. |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("If", result.signature(), condition, conditionLocation); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| |
| result.startBlock(*this, newStack); |
| if (condition.isConst() && !condition.asI32()) |
| result.setIfBranch(m_jit.jump()); // Emit direct branch if we know the condition is false. |
| else if (!condition.isConst()) // Otherwise, we only emit a branch at all if we don't know the condition statically. |
| result.setIfBranch(m_jit.branchTest32(ResultCondition::Zero, conditionLocation.asGPR())); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addElse(ControlData& data, Stack& expressionStack) |
| { |
| data.flushAndSingleExit(*this, data, expressionStack, false, true); |
| ControlData dataElse(ControlData::UseBlockCallingConventionOfOtherBranch, BlockType::Else, data); |
| data.linkJumps(&m_jit); |
| dataElse.addBranch(m_jit.jump()); |
| data.linkIfBranch(&m_jit); // Link specifically the conditional branch of the preceding If |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("Else"); |
| LOG_INDENT(); |
| |
| // We don't care at this point about the values live at the end of the previous control block, |
| // we just need the right number of temps for our arguments on the top of the stack. |
| expressionStack.clear(); |
| const auto& blockSignature = data.signature(); |
| while (expressionStack.size() < blockSignature.argumentCount()) { |
| Type type = blockSignature.argumentType(expressionStack.size()); |
| expressionStack.constructAndAppend(type, Value::fromTemp(type.kind, dataElse.enclosedHeight() + dataElse.implicitSlots() + expressionStack.size())); |
| } |
| |
| dataElse.startBlock(*this, expressionStack); |
| data = dataElse; |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addElseToUnreachable(ControlData& data) |
| { |
| // We want to flush or consume all values on the stack to reset the allocator |
| // state entering the else block. |
| data.flushAtBlockBoundary(*this, 0, m_parser->expressionStack(), true); |
| |
| ControlData dataElse(ControlData::UseBlockCallingConventionOfOtherBranch, BlockType::Else, data); |
| data.linkJumps(&m_jit); |
| dataElse.addBranch(m_jit.jump()); // Still needed even when the parent was unreachable to avoid running code within the else block. |
| data.linkIfBranch(&m_jit); // Link specifically the conditional branch of the preceding If |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("Else"); |
| LOG_INDENT(); |
| |
| // We don't have easy access to the original expression stack we had entering the if block, |
| // so we construct a local stack just to set up temp bindings as we enter the else. |
| Stack expressionStack; |
| const auto& functionSignature = dataElse.signature(); |
| for (unsigned i = 0; i < functionSignature.argumentCount(); i ++) |
| expressionStack.constructAndAppend(functionSignature.argumentType(i), Value::fromTemp(functionSignature.argumentType(i).kind, dataElse.enclosedHeight() + dataElse.implicitSlots() + i)); |
| dataElse.startBlock(*this, expressionStack); |
| data = dataElse; |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTry(BlockSignature&& signature, Stack& enclosingStack, ControlType& result, Stack& newStack) |
| { |
| m_usesExceptions = true; |
| ++m_tryCatchDepth; |
| ++m_callSiteIndex; |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::Try, WTF::move(signature), height); |
| result.setTryInfo(m_callSiteIndex, m_callSiteIndex, m_tryCatchDepth); |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("Try", result.signature()); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| result.startBlock(*this, newStack); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addTryTable(BlockSignature&& signature, Stack& enclosingStack, const Vector<CatchHandler>& targets, ControlType& result, Stack& newStack) |
| { |
| m_usesExceptions = true; |
| ++m_tryCatchDepth; |
| ++m_callSiteIndex; |
| |
| auto targetList = targets.map( |
| [&](const auto& target) -> ControlData::TryTableTarget { |
| return { |
| target.type, |
| target.tag, |
| target.exceptionSignature, |
| target.target |
| }; |
| } |
| ); |
| |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::TryTable, WTF::move(signature), height); |
| result.setTryInfo(m_callSiteIndex, m_callSiteIndex, m_tryCatchDepth); |
| result.setTryTableTargets(WTF::move(targetList)); |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("TryTable", result.signature()); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| result.startBlock(*this, newStack); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCatch(unsigned exceptionIndex, const RTT& exceptionSignature, Stack& expressionStack, ControlType& data, ResultList& results) |
| { |
| m_usesExceptions = true; |
| data.flushAndSingleExit(*this, data, expressionStack, false, true); |
| unbindAllRegisters(); |
| ControlData dataCatch(*this, BlockType::Catch, BlockSignature { data.signature() }, data.enclosedHeight()); |
| dataCatch.setCatchKind(CatchKind::Catch); |
| if (ControlData::isTry(data)) { |
| ++m_callSiteIndex; |
| data.setTryInfo(data.tryStart(), m_callSiteIndex, data.tryCatchDepth()); |
| } |
| dataCatch.setTryInfo(data.tryStart(), data.tryEnd(), data.tryCatchDepth()); |
| |
| data.delegateJumpsTo(dataCatch); |
| dataCatch.addBranch(m_jit.jump()); |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("Catch"); |
| LOG_INDENT(); |
| emitCatchImpl(dataCatch, exceptionSignature, results); |
| data = WTF::move(dataCatch); |
| m_exceptionHandlers.append({ HandlerType::Catch, data.tryStart(), data.tryEnd(), 0, m_tryCatchDepth, exceptionIndex }); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCatchToUnreachable(unsigned exceptionIndex, const RTT& exceptionSignature, ControlType& data, ResultList& results) |
| { |
| m_usesExceptions = true; |
| unbindAllRegisters(); |
| ControlData dataCatch(*this, BlockType::Catch, BlockSignature { data.signature() }, data.enclosedHeight()); |
| dataCatch.setCatchKind(CatchKind::Catch); |
| if (ControlData::isTry(data)) { |
| ++m_callSiteIndex; |
| data.setTryInfo(data.tryStart(), m_callSiteIndex, data.tryCatchDepth()); |
| } |
| dataCatch.setTryInfo(data.tryStart(), data.tryEnd(), data.tryCatchDepth()); |
| |
| data.delegateJumpsTo(dataCatch); |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("Catch"); |
| LOG_INDENT(); |
| emitCatchImpl(dataCatch, exceptionSignature, results); |
| data = WTF::move(dataCatch); |
| m_exceptionHandlers.append({ HandlerType::Catch, data.tryStart(), data.tryEnd(), 0, m_tryCatchDepth, exceptionIndex }); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCatchAll(Stack& expressionStack, ControlType& data) |
| { |
| m_usesExceptions = true; |
| data.flushAndSingleExit(*this, data, expressionStack, false, true); |
| unbindAllRegisters(); |
| ControlData dataCatch(*this, BlockType::Catch, BlockSignature { data.signature() }, data.enclosedHeight()); |
| dataCatch.setCatchKind(CatchKind::CatchAll); |
| if (ControlData::isTry(data)) { |
| ++m_callSiteIndex; |
| data.setTryInfo(data.tryStart(), m_callSiteIndex, data.tryCatchDepth()); |
| } |
| dataCatch.setTryInfo(data.tryStart(), data.tryEnd(), data.tryCatchDepth()); |
| |
| data.delegateJumpsTo(dataCatch); |
| dataCatch.addBranch(m_jit.jump()); |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("CatchAll"); |
| LOG_INDENT(); |
| emitCatchAllImpl(dataCatch); |
| data = WTF::move(dataCatch); |
| m_exceptionHandlers.append({ HandlerType::CatchAll, data.tryStart(), data.tryEnd(), 0, m_tryCatchDepth, 0 }); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCatchAllToUnreachable(ControlType& data) |
| { |
| m_usesExceptions = true; |
| unbindAllRegisters(); |
| ControlData dataCatch(*this, BlockType::Catch, BlockSignature { data.signature() }, data.enclosedHeight()); |
| dataCatch.setCatchKind(CatchKind::CatchAll); |
| if (ControlData::isTry(data)) { |
| ++m_callSiteIndex; |
| data.setTryInfo(data.tryStart(), m_callSiteIndex, data.tryCatchDepth()); |
| } |
| dataCatch.setTryInfo(data.tryStart(), data.tryEnd(), data.tryCatchDepth()); |
| |
| data.delegateJumpsTo(dataCatch); |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("CatchAll"); |
| LOG_INDENT(); |
| emitCatchAllImpl(dataCatch); |
| data = WTF::move(dataCatch); |
| m_exceptionHandlers.append({ HandlerType::CatchAll, data.tryStart(), data.tryEnd(), 0, m_tryCatchDepth, 0 }); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addDelegate(ControlType& target, ControlType& data) |
| { |
| return addDelegateToUnreachable(target, data); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addDelegateToUnreachable(ControlType& target, ControlType& data) |
| { |
| unsigned depth = 0; |
| if (ControlType::isTry(target)) |
| depth = target.tryCatchDepth(); |
| |
| if (ControlData::isTry(data)) { |
| ++m_callSiteIndex; |
| data.setTryInfo(data.tryStart(), m_callSiteIndex, data.tryCatchDepth()); |
| } |
| m_exceptionHandlers.append({ HandlerType::Delegate, data.tryStart(), m_callSiteIndex, 0, m_tryCatchDepth, depth }); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addThrow(unsigned exceptionIndex, ArgumentList& arguments, Stack&) |
| { |
| |
| LOG_INSTRUCTION("Throw", arguments); |
| |
| unsigned offset = 0; |
| for (auto arg : arguments) { |
| Location stackLocation = Location::fromStackArgument(offset * sizeof(uint64_t)); |
| emitMove(arg, stackLocation); |
| consume(arg); |
| offset += arg.value().type() == TypeKind::V128 ? 2 : 1; |
| } |
| Checked<int32_t> calleeStackSize = WTF::roundUpToMultipleOf<stackAlignmentBytes()>(offset * sizeof(uint64_t)); |
| m_maxCalleeStackSizeForValidation = std::max<uint32_t>(calleeStackSize, m_maxCalleeStackSizeForValidation); |
| ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| ++m_callSiteIndex; |
| if (m_profiledCallee.hasExceptionHandlers()) { |
| m_jit.store32(CCallHelpers::TrustedImm32(m_callSiteIndex), CCallHelpers::tagFor(CallFrameSlot::argumentCountIncludingThis)); |
| flushRegisters(); |
| } |
| m_jit.move(GPRInfo::wasmContextInstancePointer, GPRInfo::argumentGPR0); |
| emitThrowImpl(m_jit, exceptionIndex); |
| |
| return { }; |
| } |
| |
| void BBQJIT::prepareForExceptions() |
| { |
| ++m_callSiteIndex; |
| if (m_profiledCallee.hasExceptionHandlers()) { |
| m_jit.store32(CCallHelpers::TrustedImm32(m_callSiteIndex), CCallHelpers::tagFor(CallFrameSlot::argumentCountIncludingThis)); |
| flushRegistersForException(); |
| } |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addReturn(const ControlData& data, const Stack& returnValues) |
| { |
| // Use the function signature from the parser |
| ASSERT(m_parser); |
| const RTT& functionSignature = m_parser->signatureRTT(); |
| |
| CallInformation wasmCallInfo = wasmCallingConvention().callInformationFor(functionSignature, CallRole::Callee); |
| |
| if (!wasmCallInfo.results.isEmpty()) { |
| ASSERT(returnValues.size() >= wasmCallInfo.results.size()); |
| unsigned offset = returnValues.size() - wasmCallInfo.results.size(); |
| Vector<Value, 8> returnValuesForShuffle; |
| Vector<Location, 8> returnLocationsForShuffle; |
| for (unsigned i = 0; i < wasmCallInfo.results.size(); ++i) { |
| returnValuesForShuffle.append(returnValues[offset + i]); |
| returnLocationsForShuffle.append(Location::fromArgumentLocation(wasmCallInfo.results[i], returnValues[offset + i].type().kind)); |
| } |
| emitShuffle(returnValuesForShuffle, returnLocationsForShuffle); |
| LOG_INSTRUCTION("Return", returnLocationsForShuffle); |
| } else |
| LOG_INSTRUCTION("Return"); |
| |
| for (const auto& value : returnValues) |
| consume(value); |
| |
| const ControlData& enclosingBlock = !m_parser->controlStack().size() ? data : currentControlData(); |
| for (LocalOrTempIndex localIndex : enclosingBlock.m_touchedLocals) { |
| Value local = Value::fromLocal(m_localTypes[localIndex], localIndex); |
| if (locationOf(local).isRegister()) { |
| // Flush all locals without emitting stores (since we're leaving anyway) |
| unbind(local, locationOf(local)); |
| bind(local, canonicalSlot(local)); |
| } |
| } |
| |
| emitRestoreCalleeSaves(); |
| m_jit.emitFunctionEpilogue(); |
| m_jit.ret(); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addBranch(ControlData& target, Value condition, Stack& results) |
| { |
| if (condition.isConst() && !condition.asI32()) // If condition is known to be false, this is a no-op. |
| return { }; |
| |
| // It should be safe to directly use the condition location here. Between |
| // this point and when we use the condition register, we can only flush, |
| // we don't do any shuffling. Flushing will involve only the registers held |
| // by live values, and since we are about to consume the condition, its |
| // register is not one of them. The scratch register would be vulnerable to |
| // clobbering, but we don't need it - if our condition is a constant, we |
| // just fold away the branch instead of materializing it. |
| Location conditionLocation; |
| if (!condition.isNone() && !condition.isConst()) |
| conditionLocation = loadIfNecessary(condition); |
| consume(condition); |
| |
| if (condition.isNone()) |
| LOG_INSTRUCTION("Branch"); |
| else |
| LOG_INSTRUCTION("Branch", condition, conditionLocation); |
| |
| if (condition.isConst() || condition.isNone()) { |
| currentControlData().flushAndSingleExit(*this, target, results, false, condition.isNone()); |
| target.addBranch(m_jit.jump()); // We know condition is true, since if it was false we would have returned early. |
| } else { |
| currentControlData().flushAtBlockBoundary(*this, 0, results, condition.isNone()); |
| Jump ifNotTaken = m_jit.branchTest32(ResultCondition::Zero, conditionLocation.asGPR()); |
| currentControlData().addExit(*this, target.targetLocations(), results); |
| target.addBranch(m_jit.jump()); |
| ifNotTaken.link(&m_jit); |
| currentControlData().finalizeBlock(*this, target.targetLocations().size(), results, true); |
| } |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addSwitch(Value condition, const Vector<ControlData*>& targets, ControlData& defaultTarget, Stack& results) |
| { |
| ASSERT(condition.type() == TypeKind::I32); |
| |
| LOG_INSTRUCTION("BrTable", condition); |
| |
| if (!condition.isConst()) |
| emitMove(condition, Location::fromGPR(wasmScratchGPR)); |
| consume(condition); |
| |
| if (condition.isConst()) { |
| // If we know the condition statically, we emit one direct branch to the known target. |
| int targetIndex = condition.asI32(); |
| if (targetIndex >= 0 && targetIndex < static_cast<int>(targets.size())) { |
| currentControlData().flushAndSingleExit(*this, *targets[targetIndex], results, false, true); |
| targets[targetIndex]->addBranch(m_jit.jump()); |
| } else { |
| currentControlData().flushAndSingleExit(*this, defaultTarget, results, false, true); |
| defaultTarget.addBranch(m_jit.jump()); |
| } |
| return { }; |
| } |
| |
| // Flush everything below the top N values. |
| currentControlData().flushAtBlockBoundary(*this, defaultTarget.targetLocations().size(), results, true); |
| |
| constexpr unsigned minCasesForTable = 7; |
| if (minCasesForTable <= targets.size()) { |
| #if USE(JSVALUE64) |
| auto* jumpTable = m_callee.addJumpTable(targets.size() + 1); |
| m_jit.moveConditionally32(RelationalCondition::AboveOrEqual, wasmScratchGPR, TrustedImm32(targets.size()), TrustedImm32(targets.size()), wasmScratchGPR, wasmScratchGPR); |
| #else |
| auto* jumpTable = m_callee.addJumpTable(targets.size()); |
| auto fallThrough = m_jit.branch32(RelationalCondition::AboveOrEqual, wasmScratchGPR, TrustedImm32(targets.size())); |
| #endif |
| m_jit.zeroExtend32ToWord(wasmScratchGPR, wasmScratchGPR); |
| if constexpr (is64Bit()) |
| m_jit.lshiftPtr(TrustedImm32(3), wasmScratchGPR); |
| else |
| m_jit.lshiftPtr(TrustedImm32(2), wasmScratchGPR); |
| m_jit.addPtr(TrustedImmPtr(jumpTable->span().data()), wasmScratchGPR); |
| m_jit.farJump(Address(wasmScratchGPR), JSSwitchPtrTag); |
| |
| auto labels = WTF::map(targets, [&](auto& target) { |
| auto label = Box<CCallHelpers::Label>::create(m_jit.label()); |
| bool isCodeEmitted = currentControlData().addExit(*this, target->targetLocations(), results); |
| if (isCodeEmitted) |
| target->addBranch(m_jit.jump()); |
| else { |
| // It is common that we do not need to emit anything before jumping to the target block. |
| // In that case, we put Box<Label> which will be filled later when the end of the block is linked. |
| // We put direct jump to that block in the link task. |
| target->addLabel(Box { label }); |
| } |
| return label; |
| }); |
| #if USE(JSVALUE64) |
| labels.append(Box<CCallHelpers::Label>::create(m_jit.label())); |
| #else |
| fallThrough.link(&m_jit); |
| #endif |
| |
| m_jit.addLinkTask([labels = WTF::move(labels), jumpTable](LinkBuffer& linkBuffer) { |
| for (unsigned index = 0; index < labels.size(); ++index) |
| jumpTable->at(index) = linkBuffer.locationOf<JSSwitchPtrTag>(*labels[index]); |
| }); |
| } else { |
| Vector<int64_t, 16> cases(targets.size(), [](size_t i) { return i; }); |
| |
| BinarySwitch binarySwitch(wasmScratchGPR, cases.span(), BinarySwitch::Int32); |
| while (binarySwitch.advance(m_jit)) { |
| unsigned value = binarySwitch.caseValue(); |
| unsigned index = binarySwitch.caseIndex(); |
| ASSERT_UNUSED(value, value == index); |
| ASSERT(index < targets.size()); |
| currentControlData().addExit(*this, targets[index]->targetLocations(), results); |
| targets[index]->addBranch(m_jit.jump()); |
| } |
| |
| binarySwitch.fallThrough().link(&m_jit); |
| } |
| currentControlData().addExit(*this, defaultTarget.targetLocations(), results); |
| defaultTarget.addBranch(m_jit.jump()); |
| |
| currentControlData().finalizeBlock(*this, defaultTarget.targetLocations().size(), results, false); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::endBlock(ControlEntry& entry, Stack& stack) |
| { |
| return addEndToUnreachable(entry, stack, false); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addEndToUnreachable(ControlEntry& entry, Stack& stack, bool unreachable) |
| { |
| ControlData& entryData = entry.controlData; |
| |
| const auto& blockSignature = entryData.signature(); |
| unsigned returnCount = blockSignature.returnCount(); |
| if (unreachable) { |
| for (unsigned i = 0; i < returnCount; ++i) { |
| Type type = blockSignature.returnType(i); |
| entry.enclosedExpressionStack.constructAndAppend(type, Value::fromTemp(type.kind, entryData.enclosedHeight() + entryData.implicitSlots() + i)); |
| } |
| unbindAllRegisters(); |
| } else { |
| unsigned offset = stack.size() - returnCount; |
| for (unsigned i = 0; i < returnCount; ++i) |
| entry.enclosedExpressionStack.append(stack[i + offset]); |
| } |
| |
| switch (entryData.blockType()) { |
| case BlockType::TopLevel: |
| entryData.flushAndSingleExit(*this, entryData, entry.enclosedExpressionStack, false, true, unreachable); |
| entryData.linkJumps(&m_jit); |
| for (unsigned i = 0; i < returnCount; ++i) { |
| // Make sure we expect the stack values in the correct locations. |
| if (!entry.enclosedExpressionStack[i].value().isConst()) { |
| Value& value = entry.enclosedExpressionStack[i].value(); |
| value = Value::fromTemp(value.type(), i); |
| Location valueLocation = locationOf(value); |
| if (valueLocation.isRegister()) |
| RELEASE_ASSERT(valueLocation == entryData.resultLocations()[i]); |
| else |
| bind(value, entryData.resultLocations()[i]); |
| } |
| } |
| return addReturn(entryData, entry.enclosedExpressionStack); |
| case BlockType::Loop: |
| entryData.convertLoopToBlock(); |
| entryData.flushAndSingleExit(*this, entryData, entry.enclosedExpressionStack, false, true, unreachable); |
| entryData.linkJumpsTo(entryData.loopLabel(), &m_jit); |
| m_outerLoops.takeLast(); |
| break; |
| case BlockType::Try: |
| case BlockType::Catch: |
| --m_tryCatchDepth; |
| entryData.flushAndSingleExit(*this, entryData, entry.enclosedExpressionStack, false, true, unreachable); |
| entryData.linkJumps(&m_jit); |
| break; |
| case BlockType::TryTable: { |
| // normal execution: jump past the handlers |
| entryData.flushAndSingleExit(*this, entryData, entry.enclosedExpressionStack, false, true, unreachable); |
| entryData.addBranch(m_jit.jump()); |
| // similar to IPInt, we make a handler section to avoid jumping into random parts of code and not having |
| // a real landing pad |
| // FIXME: should we generate this all at the end of the code? this might help icache performance since |
| // exceptions are rare |
| ++m_callSiteIndex; |
| |
| for (auto& target : entryData.m_tryTableTargets) |
| emitCatchTableImpl(entryData, target); |
| |
| // we're done! |
| --m_tryCatchDepth; |
| entryData.linkJumps(&m_jit); |
| break; |
| } |
| default: |
| entryData.flushAndSingleExit(*this, entryData, entry.enclosedExpressionStack, false, true, unreachable); |
| entryData.linkJumps(&m_jit); |
| break; |
| } |
| |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("End"); |
| |
| currentControlData().resumeBlock(*this, entryData, entry.enclosedExpressionStack); |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::endTopLevel(const Stack&) |
| { |
| RELEASE_ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| LOG_DEDENT(); |
| LOG_INSTRUCTION("End"); |
| |
| if (m_disassembler) [[unlikely]] |
| m_disassembler->setEndOfOpcode(m_jit.label()); |
| |
| for (const auto& [ origin, latePath ] : m_latePaths) { |
| m_pcToCodeOriginMapBuilder.appendItem(m_jit.label(), CodeOrigin(BytecodeIndex(origin.m_opcodeOrigin.location()))); |
| latePath(*this, m_jit); |
| } |
| |
| for (auto& [ origin, jumpList, returnLabel, registerBindings, generator ] : m_slowPaths) { |
| JIT_COMMENT(m_jit, "Slow path start"); |
| jumpList.link(m_jit); |
| m_pcToCodeOriginMapBuilder.appendItem(m_jit.label(), CodeOrigin(BytecodeIndex(origin.m_opcodeOrigin.location()))); |
| slowPathSpillBindings(registerBindings); |
| generator(*this, m_jit); |
| slowPathRestoreBindings(registerBindings); |
| JIT_COMMENT(m_jit, "Slow path end"); |
| m_jit.jump(returnLabel); |
| } |
| |
| for (unsigned i = 0; i < numberOfExceptionTypes; ++i) { |
| auto& jumps = m_exceptions[i]; |
| if (!jumps.empty()) { |
| jumps.link(&m_jit); |
| emitThrowException(static_cast<ExceptionType>(i)); |
| } |
| } |
| |
| m_compilation->osrEntryScratchBufferSize = m_osrEntryScratchBufferSize; |
| return { }; |
| } |
| |
| // Flush a value to its canonical slot. |
| void BBQJIT::flushValue(Value value) |
| { |
| if (value.isConst() || value.isPinned()) |
| return; |
| Location currentLocation = locationOf(value); |
| Location slot = canonicalSlot(value); |
| |
| emitMove(value, slot); |
| unbind(value, currentLocation); |
| bind(value, slot); |
| } |
| |
| void BBQJIT::flush(GPRReg, const RegisterBinding& binding) |
| { |
| Value value = binding.toValue(); |
| ASSERT(value.isLocal() || value.isTemp()); |
| flushValue(value); |
| } |
| |
| void BBQJIT::flush(FPRReg, const RegisterBinding& binding) |
| { |
| Value value = binding.toValue(); |
| ASSERT(value.isLocal() || value.isTemp()); |
| flushValue(value); |
| } |
| |
| void BBQJIT::restoreWebAssemblyContextInstance() |
| { |
| m_jit.loadPtr(Address(GPRInfo::callFrameRegister, CallFrameSlot::codeBlock * sizeof(Register)), GPRInfo::wasmContextInstancePointer); |
| } |
| |
| void BBQJIT::loadWebAssemblyGlobalState(GPRReg wasmBaseMemoryPointer, GPRReg wasmBoundsCheckingSizeRegister) |
| { |
| m_jit.loadPairPtr(GPRInfo::wasmContextInstancePointer, TrustedImm32(JSWebAssemblyInstance::offsetOfCachedMemoryBaseSizePair(0)), wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister); |
| m_jit.cageConditionally(Gigacage::Primitive, wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister, wasmScratchGPR); |
| } |
| |
| void BBQJIT::flushRegistersForException() |
| { |
| // Flush all locals. |
| m_gprAllocator.flushIf(*this, [&](GPRReg, const RegisterBinding& binding) { |
| return binding.toValue().isLocal(); |
| }); |
| m_fprAllocator.flushIf(*this, [&](FPRReg, const RegisterBinding& binding) { |
| return binding.toValue().isLocal(); |
| }); |
| } |
| |
| void BBQJIT::flushRegisters() |
| { |
| // Just flush everything. |
| // FIXME: These should be store pairs. |
| m_gprAllocator.flushAllRegisters(*this); |
| m_fprAllocator.flushAllRegisters(*this); |
| } |
| |
| void BBQJIT::RegisterBindings::dump(PrintStream& out) const |
| { |
| CommaPrinter comma(", ", "["); |
| for (unsigned i = 0; i < m_fprBindings.size(); ++i) { |
| if (!m_fprBindings[i].isNone()) |
| out.print(comma, "<", static_cast<FPRReg>(i), ": ", m_fprBindings[i], ">"); |
| } |
| if (comma.didPrint()) |
| out.print("]"); |
| |
| comma = CommaPrinter(", ", comma.didPrint() ? ", ["_s : "["_s); |
| for (unsigned i = 0; i < m_gprBindings.size(); ++i) { |
| if (!m_gprBindings[i].isNone()) |
| out.print(comma, "<", static_cast<GPRReg>(i), ": ", m_gprBindings[i], ">"); |
| } |
| if (comma.didPrint()) |
| out.print("]"); |
| } |
| |
| void BBQJIT::slowPathSpillBindings(const RegisterBindings& bindings) |
| { |
| for (unsigned i = 0; i < bindings.m_fprBindings.size(); ++i) { |
| Value value = bindings.m_fprBindings[i].toValue(); |
| if (!value.isNone()) |
| emitStore(value.type(), Location::fromFPR(static_cast<FPRReg>(i)), canonicalSlot(value)); |
| } |
| |
| // FIXME: These should be store load pairs. |
| for (unsigned i = 0; i < bindings.m_gprBindings.size(); ++i) { |
| Value value = bindings.m_gprBindings[i].toValue(); |
| if (!value.isNone()) |
| emitStore(value.type(), Location::fromGPR(static_cast<GPRReg>(i)), canonicalSlot(value)); |
| } |
| } |
| |
| void BBQJIT::slowPathRestoreBindings(const RegisterBindings& bindings) |
| { |
| for (unsigned i = 0; i < bindings.m_fprBindings.size(); ++i) { |
| Value value = bindings.m_fprBindings[i].toValue(); |
| if (!value.isNone()) |
| emitLoad(value.type(), canonicalSlot(value), Location::fromFPR(static_cast<FPRReg>(i))); |
| } |
| |
| // FIXME: These should be store load pairs. |
| for (unsigned i = 0; i < bindings.m_gprBindings.size(); ++i) { |
| Value value = bindings.m_gprBindings[i].toValue(); |
| if (!value.isNone()) |
| emitLoad(value.type(), canonicalSlot(value), Location::fromGPR(static_cast<GPRReg>(i))); |
| } |
| } |
| |
| template<typename Args> |
| void BBQJIT::saveValuesAcrossCallAndPassArguments(const Args& arguments, const CallInformation& callInfo, const RTT& signature) |
| { |
| // First, we resolve all the locations of the passed arguments, before any spillage occurs. For constants, |
| // we store their normal values; for all other values, we store pinned values with their current location. |
| // We'll directly use these when passing parameters, since no other instructions we emit here should |
| // overwrite registers currently occupied by values. |
| |
| auto resolvedArguments = WTF::map<8>(arguments, [&](auto& input) { |
| auto argument = Value(input); |
| auto value = argument.isConst() ? argument : Value::pinned(argument.type(), locationOf(argument)); |
| // Like other value uses, we count this as a use here, and end the lifetimes of any temps we passed. |
| // This saves us the work of having to spill them to their canonical slots. |
| consume(argument); |
| return value; |
| }); |
| |
| // At this point in the program, argumentLocations doesn't represent the state of the register allocator. |
| // We need to be careful not to allocate any new registers before passing them to the function, since that |
| // could clobber the registers we assume still contain the argument values! |
| |
| // Next, for all values currently still occupying a caller-saved register, we flush them to their canonical slot. |
| for (Reg reg : m_callerSaves) { |
| RegisterBinding binding = bindingFor(reg); |
| ASSERT(!binding.isScratch()); |
| if (!binding.toValue().isNone()) |
| flushValue(binding.toValue()); |
| } |
| |
| // Additionally, we flush anything currently bound to a register we're going to use for parameter passing. I |
| // think these will be handled by the caller-save logic without additional effort, but it doesn't hurt to be |
| // careful. |
| for (size_t i = 0; i < callInfo.params.size(); ++i) { |
| auto type = signature.argumentType(i); |
| Location paramLocation = Location::fromArgumentLocation(callInfo.params[i], type.kind); |
| if (paramLocation.isRegister()) { |
| RegisterBinding binding; |
| if (paramLocation.isGPR()) |
| binding = bindingFor(paramLocation.asGPR()); |
| else if (paramLocation.isFPR()) |
| binding = bindingFor(paramLocation.asFPR()); |
| else if (paramLocation.isGPR2()) |
| binding = bindingFor(paramLocation.asGPRhi()); |
| if (!binding.toValue().isNone()) |
| flushValue(binding.toValue()); |
| } |
| } |
| |
| // Finally, we parallel-move arguments to the parameter locations. |
| WTF::Vector<Location, 8> parameterLocations; |
| parameterLocations.reserveInitialCapacity(callInfo.params.size()); |
| for (unsigned i = 0; i < callInfo.params.size(); i++) { |
| auto type = signature.argumentType(i); |
| auto parameterLocation = Location::fromArgumentLocation(callInfo.params[i], type.kind); |
| parameterLocations.append(parameterLocation); |
| } |
| |
| emitShuffle(resolvedArguments, parameterLocations); |
| } |
| |
| void BBQJIT::restoreValuesAfterCall(const CallInformation& callInfo) |
| { |
| UNUSED_PARAM(callInfo); |
| // Caller-saved values shouldn't actually need to be restored here, the register allocator will restore them lazily |
| // whenever they are next used. |
| } |
| |
| template<size_t N> |
| void BBQJIT::returnValuesFromCall(Vector<Value, N>& results, const RTT& functionType, const CallInformation& callInfo) |
| { |
| for (size_t i = 0; i < callInfo.results.size(); i ++) { |
| Value result = topValue(functionType.returnType(i).kind, i); |
| Location returnLocation = Location::fromArgumentLocation(callInfo.results[i], result.type()); |
| if (returnLocation.isRegister()) { |
| RegisterBinding currentBinding; |
| if (returnLocation.isGPR()) |
| currentBinding = bindingFor(returnLocation.asGPR()); |
| else if (returnLocation.isFPR()) |
| currentBinding = bindingFor(returnLocation.asFPR()); |
| else if (returnLocation.isGPR2()) |
| currentBinding = bindingFor(returnLocation.asGPRhi()); |
| // There's no way to preserve an abritrary scratch over a call so we shouldn't try to do so. |
| ASSERT(!currentBinding.isScratch()); |
| } else { |
| ASSERT(returnLocation.isStackArgument()); |
| Location canonicalLocation = canonicalSlot(result); |
| emitMoveMemory(result.type(), returnLocation, canonicalLocation); |
| returnLocation = canonicalLocation; |
| } |
| bind(result, returnLocation); |
| results.append(result); |
| } |
| } |
| |
| void BBQJIT::emitTailCall(FunctionSpaceIndex functionIndexSpace, const RTT& signature, ArgumentList& arguments) |
| { |
| const auto& callingConvention = wasmCallingConvention(); |
| CallInformation callInfo = callingConvention.callInformationFor(signature, CallRole::Callee); |
| Checked<int32_t> calleeStackSize = WTF::roundUpToMultipleOf(stackAlignmentBytes(), callInfo.headerAndArgumentStackSizeInBytes); |
| // Do this to ensure we don't write past SP. |
| m_maxCalleeStackSizeForValidation = std::max<uint32_t>(calleeStackSize, m_maxCalleeStackSizeForValidation); |
| ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| const TypeSignatureIndex callerTypeSignatureIndex = m_info.internalFunctionTypeSignatureIndices[m_functionIndex]; |
| const RTT& callerType = m_info.rtt(callerTypeSignatureIndex); |
| CallInformation wasmCallerInfo = callingConvention.callInformationFor(callerType, CallRole::Callee); |
| Checked<int32_t> callerStackSize = WTF::roundUpToMultipleOf(stackAlignmentBytes(), wasmCallerInfo.headerAndArgumentStackSizeInBytes); |
| Checked<int32_t> tailCallStackOffsetFromFP = callerStackSize - calleeStackSize; |
| ASSERT(callInfo.results.size() == wasmCallerInfo.results.size()); |
| ASSERT(arguments.size() == callInfo.params.size()); |
| |
| Vector<Value, 8> resolvedArguments; |
| resolvedArguments.reserveInitialCapacity(arguments.size() + isX86()); |
| Vector<Location, 8> parameterLocations; |
| parameterLocations.reserveInitialCapacity(arguments.size() + isX86()); |
| |
| // Save the old Frame Pointer for later and make sure the return address gets saved to its canonical location. |
| emitRestoreCalleeSaves(); |
| auto preserved = callingConvention.argumentGPRs(); |
| if constexpr (isARM64E()) |
| preserved.add(callingConvention.prologueScratchGPRs[0], IgnoreVectors); |
| ScratchScope<1, 0> scratches(*this, WTF::move(preserved)); |
| GPRReg callerFramePointer = scratches.gpr(0); |
| scratches.unbindPreserved(); |
| |
| #if CPU(X86_64) |
| m_jit.loadPtr(Address(MacroAssembler::framePointerRegister), callerFramePointer); |
| resolvedArguments.append(Value::pinned(pointerType(), Location::fromStack(sizeof(Register)))); |
| parameterLocations.append(Location::fromStack(tailCallStackOffsetFromFP + Checked<int>(sizeof(Register)))); |
| #elif CPU(ARM64) || CPU(ARM_THUMB2) |
| m_jit.loadPairPtr(MacroAssembler::framePointerRegister, callerFramePointer, MacroAssembler::linkRegister); |
| #else |
| UNUSED_PARAM(callerFramePointer); |
| UNREACHABLE_FOR_PLATFORM(); |
| #endif |
| |
| // We don't need to restore any callee saves because we don't use them with the current register allocator. |
| // If we did we'd want to do that here because we could clobber their stack slots when shuffling the parameters into place below. |
| for (unsigned i = 0; i < arguments.size(); i ++) { |
| if (arguments[i].value().isConst()) |
| resolvedArguments.append(arguments[i].value()); |
| else |
| resolvedArguments.append(Value::pinned(arguments[i].value().type(), locationOf(arguments[i]))); |
| |
| consume(arguments[i]); |
| } |
| |
| for (size_t i = 0; i < callInfo.params.size(); ++i) { |
| auto param = callInfo.params[i]; |
| switch (param.location.kind()) { |
| case ValueLocation::Kind::GPRRegister: |
| case ValueLocation::Kind::FPRRegister: { |
| auto type = signature.argumentType(i); |
| parameterLocations.append(Location::fromArgumentLocation(param, type.kind)); |
| break; |
| } |
| case ValueLocation::Kind::StackArgument: |
| RELEASE_ASSERT_NOT_REACHED(); |
| break; |
| case ValueLocation::Kind::Stack: |
| parameterLocations.append(Location::fromStack(param.location.offsetFromFP() + tailCallStackOffsetFromFP)); |
| break; |
| } |
| } |
| |
| emitShuffle(resolvedArguments, parameterLocations); |
| |
| #if CPU(ARM64E) |
| JIT_COMMENT(m_jit, "Untag our return PC"); |
| // prologue scratch registers should be free as we already moved the arguments into place. |
| GPRReg scratch = callingConvention.prologueScratchGPRs[0]; |
| m_jit.addPtr(TrustedImm32(sizeof(CallerFrameAndPC)), MacroAssembler::framePointerRegister, scratch); |
| m_jit.untagPtr(scratch, ARM64Registers::lr); |
| m_jit.validateUntaggedPtr(ARM64Registers::lr, scratch); |
| #endif |
| |
| // Fix SP and FP |
| m_jit.addPtr(TrustedImm32(tailCallStackOffsetFromFP + Checked<int32_t>(prologueStackPointerDelta())), MacroAssembler::framePointerRegister, MacroAssembler::stackPointerRegister); |
| m_jit.move(callerFramePointer, MacroAssembler::framePointerRegister); |
| |
| // Nothing should refer to FP after this point. |
| |
| if (m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace)) { |
| static_assert(sizeof(WasmOrJSImportableFunctionCallLinkInfo) * maxImports < std::numeric_limits<int32_t>::max()); |
| RELEASE_ASSERT(JSWebAssemblyInstance::offsetOfImportFunctionStub(m_module.moduleInformation(), functionIndexSpace) < std::numeric_limits<int32_t>::max()); |
| m_jit.farJump(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfImportFunctionStub(m_module.moduleInformation(), functionIndexSpace)), WasmEntryPtrTag); |
| } else { |
| // Record the callee so the callee knows to look for it in updateCallsitesToCallUs. |
| m_directCallees.testAndSet(m_info.toCodeIndex(functionIndexSpace)); |
| // Emit the call. |
| Vector<UnlinkedWasmToWasmCall>* unlinkedWasmToWasmCalls = &m_unlinkedWasmToWasmCalls; |
| ASSERT(!m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace)); |
| Ref<IPIntCallee> callee = m_calleeGroup.ipintCalleeFromFunctionIndexSpace(functionIndexSpace); |
| m_jit.storeWasmCalleeToCalleeCallFrame(TrustedImmPtr(CalleeBits::boxNativeCallee(callee.ptr())), sizeof(CallerFrameAndPC) - prologueStackPointerDelta()); |
| CCallHelpers::Call call = m_jit.threadSafePatchableNearTailCall(); |
| |
| m_jit.addLinkTask([unlinkedWasmToWasmCalls, call, functionIndexSpace] (LinkBuffer& linkBuffer) { |
| unlinkedWasmToWasmCalls->append({ linkBuffer.locationOfNearCall<WasmEntryPtrTag>(call), functionIndexSpace }); |
| }); |
| } |
| |
| LOG_INSTRUCTION("ReturnCall", functionIndexSpace, arguments); |
| |
| // Because we're returning, we need to unbind all elements of the |
| // expression stack here so they don't spuriously hold onto their bindings |
| // in the subsequent unreachable code. |
| unbindAllRegisters(); |
| } |
| |
| |
| [[nodiscard]] PartialResult BBQJIT::addCall(unsigned callProfileIndex, FunctionSpaceIndex functionIndexSpace, const RTT& signature, ArgumentList& arguments, ResultList& results, CallType callType) |
| { |
| emitIncrementCallProfileCount(callProfileIndex); |
| JIT_COMMENT(m_jit, "calling functionIndexSpace: ", functionIndexSpace, ConditionalDump(!m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace), " functionIndex: ", functionIndexSpace - m_info.importFunctionCount())); |
| |
| if (callType == CallType::TailCall) { |
| emitTailCall(functionIndexSpace, signature, arguments); |
| return { }; |
| } |
| |
| CallInformation callInfo = wasmCallingConvention().callInformationFor(signature, CallRole::Caller); |
| Checked<int32_t> calleeStackSize = WTF::roundUpToMultipleOf<stackAlignmentBytes()>(callInfo.headerAndArgumentStackSizeInBytes); |
| m_maxCalleeStackSizeForValidation = std::max<uint32_t>(calleeStackSize, m_maxCalleeStackSizeForValidation); |
| ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| // Preserve caller-saved registers and other info |
| prepareForExceptions(); |
| saveValuesAcrossCallAndPassArguments(arguments, callInfo, signature); |
| |
| if (m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace)) { |
| static_assert(sizeof(WasmOrJSImportableFunctionCallLinkInfo) * maxImports < std::numeric_limits<int32_t>::max()); |
| RELEASE_ASSERT(JSWebAssemblyInstance::offsetOfImportFunctionStub(m_module.moduleInformation(), functionIndexSpace) < std::numeric_limits<int32_t>::max()); |
| m_jit.call(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfImportFunctionStub(m_module.moduleInformation(), functionIndexSpace)), WasmEntryPtrTag); |
| } else { |
| // Record the callee so the callee knows to look for it in updateCallsitesToCallUs. |
| ASSERT(m_info.toCodeIndex(functionIndexSpace) < m_info.internalFunctionCount()); |
| m_directCallees.testAndSet(m_info.toCodeIndex(functionIndexSpace)); |
| // Emit the call. |
| Vector<UnlinkedWasmToWasmCall>* unlinkedWasmToWasmCalls = &m_unlinkedWasmToWasmCalls; |
| ASSERT(!m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace)); |
| Ref<IPIntCallee> callee = m_calleeGroup.ipintCalleeFromFunctionIndexSpace(functionIndexSpace); |
| m_jit.storeWasmCalleeToCalleeCallFrame(TrustedImmPtr(CalleeBits::boxNativeCallee(callee.ptr())), 0); |
| CCallHelpers::Call call = m_jit.threadSafePatchableNearCall(); |
| |
| m_jit.addLinkTask([unlinkedWasmToWasmCalls, call, functionIndexSpace] (LinkBuffer& linkBuffer) { |
| unlinkedWasmToWasmCalls->append({ linkBuffer.locationOfNearCall<WasmEntryPtrTag>(call), functionIndexSpace }); |
| }); |
| } |
| |
| // Push return value(s) onto the expression stack. Read results before restoring SP |
| // since results are at the bottom of the arg/result area, addressable from the callee's SP. |
| returnValuesFromCall(results, signature, callInfo); |
| |
| // Our callee could have tail called someone else and changed SP so we need to restore it. |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(m_frameSize), MacroAssembler::stackPointerRegister); |
| |
| if (m_info.callCanClobberInstance(functionIndexSpace) || m_info.isImportedFunctionFromFunctionIndexSpace(functionIndexSpace)) |
| restoreWebAssemblyGlobalStateAfterWasmCall(); |
| |
| LOG_INSTRUCTION("Call", functionIndexSpace, arguments, "=> ", results); |
| |
| return { }; |
| } |
| |
| void BBQJIT::emitIndirectCall(const char* opcode, unsigned callProfileIndex, const Value& callee, GPRReg importableFunction, const RTT& signature, ArgumentList& arguments, ResultList& results) |
| { |
| ASSERT(importableFunction == GPRInfo::nonPreservedNonArgumentGPR1); |
| ASSERT(!RegisterSet::argumentGPRs().contains(importableFunction, IgnoreVectors)); |
| ASSERT(!RegisterSet::argumentGPRs().contains(wasmScratchGPR, IgnoreVectors)); |
| |
| const auto& callingConvention = wasmCallingConvention(); |
| CallInformation wasmCalleeInfo = callingConvention.callInformationFor(signature, CallRole::Caller); |
| Checked<int32_t> calleeStackSize = WTF::roundUpToMultipleOf<stackAlignmentBytes()>(wasmCalleeInfo.headerAndArgumentStackSizeInBytes); |
| m_maxCalleeStackSizeForValidation = std::max<uint32_t>(calleeStackSize, m_maxCalleeStackSizeForValidation); |
| ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| prepareForExceptions(); |
| saveValuesAcrossCallAndPassArguments(arguments, wasmCalleeInfo, signature); // Keep in mind that this clobbers wasmScratchGPR and wasmScratchFPR. |
| |
| // Now, all argument GPRs are set up, but |
| // 1. Callee save registers are kept, thus we can access GPRInfo::jitDataRegister. |
| // 2. boxedCallee is placed in GPRInfo::nonPreservedNonArgumentGPR1, so not clobbered via arguments set up. |
| // 3. wasmScratchGPR can be clobbered now. |
| |
| JumpList afterCall; |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfBoxedCallee()), wasmScratchGPR); |
| m_jit.storeWasmCalleeToCalleeCallFrame(wasmScratchGPR); |
| if (m_profile->isMegamorphic(callProfileIndex)) { |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfTargetInstance()), wasmScratchGPR); |
| // Do a context switch if needed. |
| Jump isSameInstanceBefore = m_jit.branchPtr(RelationalCondition::Equal, wasmScratchGPR, GPRInfo::wasmContextInstancePointer); |
| m_jit.move(wasmScratchGPR, GPRInfo::wasmContextInstancePointer); |
| #if USE(JSVALUE64) |
| loadWebAssemblyGlobalState(wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister); |
| #endif |
| isSameInstanceBefore.link(m_jit); |
| } else { |
| JumpList profilingDone; |
| JumpList updateProfile; |
| |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfTargetInstance()), m_jit.scratchRegister()); |
| Jump isSameInstanceBefore = m_jit.branchPtr(RelationalCondition::Equal, m_jit.scratchRegister(), GPRInfo::wasmContextInstancePointer); |
| m_jit.move(m_jit.scratchRegister(), GPRInfo::wasmContextInstancePointer); |
| #if USE(JSVALUE64) |
| loadWebAssemblyGlobalState(wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister); |
| #endif |
| m_jit.loadPtr(CCallHelpers::Address(GPRInfo::jitDataRegister, safeCast<int32_t>(BaselineData::offsetOfData() + sizeof(CallProfile) * callProfileIndex + CallProfile::offsetOfBoxedCallee())), wasmScratchGPR); |
| m_jit.orPtr(TrustedImm32(CallProfile::Megamorphic), wasmScratchGPR); |
| updateProfile.append(m_jit.jump()); |
| |
| isSameInstanceBefore.link(m_jit); |
| m_jit.loadPtr(CCallHelpers::Address(GPRInfo::jitDataRegister, safeCast<int32_t>(BaselineData::offsetOfData() + sizeof(CallProfile) * callProfileIndex + CallProfile::offsetOfBoxedCallee())), m_jit.scratchRegister()); |
| profilingDone.append(m_jit.branchPtr(CCallHelpers::Equal, wasmScratchGPR, m_jit.scratchRegister())); |
| profilingDone.append(m_jit.branchTestPtr(CCallHelpers::NonZero, m_jit.scratchRegister(), TrustedImm32(CallProfile::Megamorphic))); |
| |
| updateProfile.append(m_jit.branchTestPtr(CCallHelpers::Zero, m_jit.scratchRegister())); |
| m_jit.addPtr(TrustedImm32(safeCast<int32_t>(BaselineData::offsetOfData() + sizeof(CallProfile) * callProfileIndex)), GPRInfo::jitDataRegister, GPRInfo::wasmContextInstancePointer); |
| m_jit.nearCallThunk(CodeLocationLabel<JITThunkPtrTag>(Thunks::singleton().stub(callPolymorphicCalleeGenerator).code())); |
| afterCall.append(m_jit.jump()); |
| |
| updateProfile.link(&m_jit); |
| m_jit.storePtr(wasmScratchGPR, CCallHelpers::Address(GPRInfo::jitDataRegister, safeCast<int32_t>(BaselineData::offsetOfData() + sizeof(CallProfile) * callProfileIndex + CallProfile::offsetOfBoxedCallee()))); |
| profilingDone.link(m_jit); |
| } |
| |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfEntrypointLoadLocation()), wasmScratchGPR); |
| m_jit.call(CCallHelpers::Address(wasmScratchGPR), WasmEntryPtrTag); |
| |
| // Read results before restoring SP since results are at the bottom of the |
| // arg/result area, addressable from the callee's SP. |
| afterCall.link(m_jit); |
| returnValuesFromCall(results, signature, wasmCalleeInfo); |
| |
| // Our callee could have tail called someone else and changed SP so we need to restore it. |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(m_frameSize), MacroAssembler::stackPointerRegister); |
| |
| restoreWebAssemblyGlobalStateAfterWasmCall(); |
| |
| LOG_INSTRUCTION(opcode, callee, arguments, "=> ", results); |
| } |
| |
| void BBQJIT::emitIndirectTailCall(const char* opcode, const Value& callee, GPRReg importableFunction, const RTT& signature, ArgumentList& arguments) |
| { |
| ASSERT(!RegisterSet::argumentGPRs().contains(importableFunction, IgnoreVectors)); |
| ASSERT(!RegisterSet::argumentGPRs().contains(wasmScratchGPR, IgnoreVectors)); |
| |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfBoxedCallee()), wasmScratchGPR); |
| m_jit.storeWasmCalleeToCalleeCallFrame(wasmScratchGPR); |
| |
| // Do a context switch if needed. |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfTargetInstance()), wasmScratchGPR); |
| Jump isSameInstanceBefore = m_jit.branchPtr(RelationalCondition::Equal, wasmScratchGPR, GPRInfo::wasmContextInstancePointer); |
| m_jit.move(wasmScratchGPR, GPRInfo::wasmContextInstancePointer); |
| #if USE(JSVALUE64) |
| loadWebAssemblyGlobalState(wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister); |
| #endif |
| isSameInstanceBefore.link(&m_jit); |
| |
| const auto& callingConvention = wasmCallingConvention(); |
| CallInformation callInfo = callingConvention.callInformationFor(signature, CallRole::Callee); |
| Checked<int32_t> calleeStackSize = WTF::roundUpToMultipleOf(stackAlignmentBytes(), callInfo.headerAndArgumentStackSizeInBytes); |
| // Do this to ensure we don't write past SP. |
| m_maxCalleeStackSizeForValidation = std::max<uint32_t>(calleeStackSize, m_maxCalleeStackSizeForValidation); |
| ASSERT(static_cast<uint32_t>(alignedFrameSize(m_maxCalleeStackSizeForValidation + m_frameSizeForValidation)) <= m_frameSize); |
| |
| const TypeSignatureIndex callerTypeSignatureIndex = m_info.internalFunctionTypeSignatureIndices[m_functionIndex]; |
| const RTT& callerType = m_info.rtt(callerTypeSignatureIndex); |
| CallInformation wasmCallerInfo = callingConvention.callInformationFor(callerType, CallRole::Callee); |
| Checked<int32_t> callerStackSize = WTF::roundUpToMultipleOf(stackAlignmentBytes(), wasmCallerInfo.headerAndArgumentStackSizeInBytes); |
| Checked<int32_t> tailCallStackOffsetFromFP = callerStackSize - calleeStackSize; |
| ASSERT(callInfo.results.size() == wasmCallerInfo.results.size()); |
| ASSERT(arguments.size() == callInfo.params.size()); |
| |
| Vector<Value, 8> resolvedArguments; |
| const unsigned calleeArgument = 1; |
| resolvedArguments.reserveInitialCapacity(arguments.size() + calleeArgument + isX86() * 2); |
| Vector<Location, 8> parameterLocations; |
| parameterLocations.reserveInitialCapacity(arguments.size() + calleeArgument + isX86() * 2); |
| |
| // It's ok if we clobber our Wasm::Callee at this point since we can't hit a GC safepoint / throw an exception until we've tail called into the callee. |
| // FIXME: We should just have addCallIndirect put this in the right place to begin with. |
| resolvedArguments.append(Value::pinned(TypeKind::I64, Location::fromStackArgument(CCallHelpers::addressOfCalleeCalleeFromCallerPerspective(0).offset))); |
| parameterLocations.append(Location::fromStack(tailCallStackOffsetFromFP + Checked<int>(CallFrameSlot::callee * sizeof(Register)))); |
| |
| // Save the old Frame Pointer for later and make sure the return address gets saved to its canonical location. |
| emitRestoreCalleeSaves(); |
| #if CPU(X86_64) |
| // There are no remaining non-argument non-preserved gprs left on X86_64 so we have to shuffle FP to a temp slot. |
| resolvedArguments.append(Value::pinned(pointerType(), Location::fromStack(0))); |
| parameterLocations.append(Location::fromStack(tailCallStackOffsetFromFP)); |
| |
| resolvedArguments.append(Value::pinned(pointerType(), Location::fromStack(sizeof(Register)))); |
| parameterLocations.append(Location::fromStack(tailCallStackOffsetFromFP + Checked<int>(sizeof(Register)))); |
| #elif CPU(ARM64) || CPU(ARM_THUMB2) |
| auto preserved = callingConvention.argumentGPRs(); |
| preserved.add(importableFunction, IgnoreVectors); |
| if constexpr (isARM64E()) |
| preserved.add(callingConvention.prologueScratchGPRs[0], IgnoreVectors); |
| ScratchScope<1, 0> scratches(*this, WTF::move(preserved)); |
| GPRReg callerFramePointer = scratches.gpr(0); |
| scratches.unbindPreserved(); |
| m_jit.loadPairPtr(MacroAssembler::framePointerRegister, callerFramePointer, MacroAssembler::linkRegister); |
| #else |
| UNREACHABLE_FOR_PLATFORM(); |
| #endif |
| |
| for (unsigned i = 0; i < arguments.size(); i ++) { |
| if (arguments[i].value().isConst()) |
| resolvedArguments.append(arguments[i].value()); |
| else |
| resolvedArguments.append(Value::pinned(arguments[i].value().type(), locationOf(arguments[i]))); |
| |
| // This isn't really needed but it's nice to have good book keeping. |
| consume(arguments[i]); |
| } |
| |
| for (size_t i = 0; i < callInfo.params.size(); ++i) { |
| auto param = callInfo.params[i]; |
| switch (param.location.kind()) { |
| case ValueLocation::Kind::GPRRegister: |
| case ValueLocation::Kind::FPRRegister: { |
| auto type = signature.argumentType(i); |
| parameterLocations.append(Location::fromArgumentLocation(param, type.kind)); |
| break; |
| } |
| case ValueLocation::Kind::StackArgument: |
| RELEASE_ASSERT_NOT_REACHED(); |
| break; |
| case ValueLocation::Kind::Stack: |
| parameterLocations.append(Location::fromStack(param.location.offsetFromFP() + tailCallStackOffsetFromFP)); |
| break; |
| } |
| } |
| |
| emitShuffle(resolvedArguments, parameterLocations); |
| |
| #if CPU(ARM64E) |
| JIT_COMMENT(m_jit, "Untag our return PC"); |
| // prologue scratch registers should be free as we already moved the arguments into place. |
| GPRReg scratch = callingConvention.prologueScratchGPRs[0]; |
| m_jit.addPtr(TrustedImm32(sizeof(CallerFrameAndPC)), MacroAssembler::framePointerRegister, scratch); |
| m_jit.untagPtr(scratch, ARM64Registers::lr); |
| m_jit.validateUntaggedPtr(ARM64Registers::lr, scratch); |
| #endif |
| |
| // Fix SP and FP |
| #if CPU(X86_64) |
| m_jit.loadPtr(Address(MacroAssembler::framePointerRegister, tailCallStackOffsetFromFP), wasmScratchGPR); |
| m_jit.addPtr(TrustedImm32(tailCallStackOffsetFromFP + Checked<int>(sizeof(Register))), MacroAssembler::framePointerRegister, MacroAssembler::stackPointerRegister); |
| m_jit.move(wasmScratchGPR, MacroAssembler::framePointerRegister); |
| #elif CPU(ARM64) || CPU(ARM_THUMB2) |
| m_jit.addPtr(TrustedImm32(tailCallStackOffsetFromFP + Checked<int>(sizeof(CallerFrameAndPC))), MacroAssembler::framePointerRegister, MacroAssembler::stackPointerRegister); |
| m_jit.move(callerFramePointer, MacroAssembler::framePointerRegister); |
| #else |
| UNREACHABLE_FOR_PLATFORM(); |
| #endif |
| |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfEntrypointLoadLocation()), importableFunction); |
| m_jit.farJump(CCallHelpers::Address(importableFunction), WasmEntryPtrTag); |
| LOG_INSTRUCTION(opcode, callee, arguments); |
| |
| // Because we're returning, we need to consume all elements of the |
| // expression stack here so they don't spuriously hold onto their bindings |
| // in the subsequent unreachable code. |
| |
| for (const auto& value : m_parser->expressionStack()) |
| consume(value); |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCallIndirect(unsigned callProfileIndex, unsigned tableIndex, const RTT& signature, ArgumentList& args, ResultList& results, CallType callType) |
| { |
| emitIncrementCallProfileCount(callProfileIndex); |
| Value calleeIndex = args.takeLast(); |
| ASSERT(signature.argumentCount() == args.size()); |
| ASSERT(m_info.tableCount() > tableIndex); |
| ASSERT(m_info.tables[tableIndex].type() == TableElementType::Funcref); |
| |
| Location calleeIndexLocation; |
| GPRReg importableFunction = GPRInfo::nonPreservedNonArgumentGPR1; |
| { |
| clobber(importableFunction); |
| ScratchScope<0, 0> importableFunctionScope(*this, importableFunction); |
| static_assert(GPRInfo::nonPreservedNonArgumentGPR0 == wasmScratchGPR); |
| |
| { |
| ScratchScope<2, 0> scratches(*this); |
| |
| if (calleeIndex.isConst()) |
| emitMoveConst(calleeIndex, calleeIndexLocation = Location::fromGPR(scratches.gpr(1))); |
| else |
| calleeIndexLocation = loadIfNecessary(calleeIndex); |
| |
| GPRReg callableFunctionBuffer = scratches.gpr(0); |
| |
| ASSERT(tableIndex < m_info.tableCount()); |
| |
| auto& tableInformation = m_info.table(tableIndex); |
| if (tableInformation.maximum() && tableInformation.maximum().value() == tableInformation.initial()) { |
| if (!tableIndex) |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfCachedTable0Buffer()), callableFunctionBuffer); |
| else { |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfTable(m_info, tableIndex)), wasmScratchGPR); |
| if (!tableInformation.isImport()) |
| m_jit.addPtr(TrustedImm32(FuncRefTable::offsetOfFunctionsForFixedSizedTable()), wasmScratchGPR, callableFunctionBuffer); |
| else |
| m_jit.loadPtr(Address(wasmScratchGPR, FuncRefTable::offsetOfFunctions()), callableFunctionBuffer); |
| } |
| m_jit.move(TrustedImm32(tableInformation.initial()), wasmScratchGPR); |
| } else { |
| if (!tableIndex) { |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfCachedTable0Buffer()), callableFunctionBuffer); |
| m_jit.load32(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfCachedTable0Length()), wasmScratchGPR); |
| } else { |
| m_jit.loadPtr(Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfTable(m_info, tableIndex)), wasmScratchGPR); |
| m_jit.loadPtr(Address(wasmScratchGPR, FuncRefTable::offsetOfFunctions()), callableFunctionBuffer); |
| m_jit.load32(Address(wasmScratchGPR, FuncRefTable::offsetOfLength()), wasmScratchGPR); |
| } |
| } |
| ASSERT(calleeIndexLocation.isGPR()); |
| |
| JIT_COMMENT(m_jit, "Check the index we are looking for is valid"); |
| recordJumpToThrowException(ExceptionType::OutOfBoundsCallIndirect, m_jit.branch32(RelationalCondition::AboveOrEqual, calleeIndexLocation.asGPR(), wasmScratchGPR)); |
| |
| // Neither callableFunctionBuffer nor wasmScratchGPR are used before any of these |
| // are def'd below, so we can reuse the registers and save some pressure. |
| |
| static_assert(sizeof(TypeIndex) == sizeof(void*)); |
| |
| JIT_COMMENT(m_jit, "Compute the offset in the table index space we are looking for"); |
| if constexpr (hasOneBitSet(sizeof(FuncRefTable::Function))) { |
| m_jit.zeroExtend32ToWord(calleeIndexLocation.asGPR(), calleeIndexLocation.asGPR()); |
| #if CPU(ARM64) |
| m_jit.addLeftShift64(callableFunctionBuffer, calleeIndexLocation.asGPR(), TrustedImm32(getLSBSet(sizeof(FuncRefTable::Function))), importableFunction); |
| #elif CPU(ARM) |
| m_jit.lshift32(calleeIndexLocation.asGPR(), TrustedImm32(getLSBSet(sizeof(FuncRefTable::Function))), importableFunction); |
| m_jit.addPtr(callableFunctionBuffer, importableFunction); |
| #else |
| m_jit.lshift64(calleeIndexLocation.asGPR(), TrustedImm32(getLSBSet(sizeof(FuncRefTable::Function))), importableFunction); |
| m_jit.addPtr(callableFunctionBuffer, importableFunction); |
| #endif |
| } else { |
| m_jit.move(TrustedImmPtr(sizeof(FuncRefTable::Function)), importableFunction); |
| #if CPU(ARM64) |
| m_jit.multiplyAddZeroExtend32(calleeIndexLocation.asGPR(), importableFunction, callableFunctionBuffer, importableFunction); |
| #elif CPU(ARM) |
| m_jit.mul32(calleeIndexLocation.asGPR(), importableFunction); |
| m_jit.addPtr(callableFunctionBuffer, importableFunction); |
| #else |
| m_jit.zeroExtend32ToWord(calleeIndexLocation.asGPR(), calleeIndexLocation.asGPR()); |
| m_jit.mul64(calleeIndexLocation.asGPR(), importableFunction); |
| m_jit.addPtr(callableFunctionBuffer, importableFunction); |
| #endif |
| } |
| consume(calleeIndex); |
| |
| // FIXME: This seems wasteful to do two checks just for a nicer error message. |
| // We should move just to use a single branch and then figure out what |
| // error to use in the exception handler. |
| |
| m_jit.loadPtr(CCallHelpers::Address(importableFunction, WasmToWasmImportableFunction::offsetOfRTT()), wasmScratchGPR); |
| if (signature.isFinalType()) |
| recordJumpToThrowException(ExceptionType::BadSignature, m_jit.branchPtr(CCallHelpers::NotEqual, wasmScratchGPR, TrustedImmPtr(&signature))); |
| else { |
| auto indexEqual = m_jit.branchPtr(CCallHelpers::Equal, wasmScratchGPR, TrustedImmPtr(&signature)); |
| recordJumpToThrowException(ExceptionType::BadSignature, m_jit.branchTestPtr(ResultCondition::Zero, wasmScratchGPR)); |
| recordJumpToThrowException(ExceptionType::BadSignature, m_jit.branch32(CCallHelpers::BelowOrEqual, Address(wasmScratchGPR, RTT::offsetOfDisplaySizeExcludingThis()), TrustedImm32(signature.displaySizeExcludingThis()))); |
| recordJumpToThrowException(ExceptionType::BadSignature, m_jit.branchPtr(CCallHelpers::NotEqual, CCallHelpers::Address(wasmScratchGPR, RTT::offsetOfData() + signature.displaySizeExcludingThis() * sizeof(RefPtr<const RTT>)), TrustedImmPtr(&signature))); |
| |
| indexEqual.link(&m_jit); |
| } |
| } |
| } |
| |
| JIT_COMMENT(m_jit, "Finished loading callee code"); |
| if (callType == CallType::Call) |
| emitIndirectCall("CallIndirect", callProfileIndex, calleeIndex, importableFunction, signature, args, results); |
| else |
| emitIndirectTailCall("ReturnCallIndirect", calleeIndex, importableFunction, signature, args); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addUnreachable() |
| { |
| LOG_INSTRUCTION("Unreachable"); |
| emitThrowException(ExceptionType::Unreachable); |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addCrash() |
| { |
| m_jit.breakpoint(); |
| return { }; |
| } |
| |
| WasmOrigin BBQJIT::origin() |
| { |
| if (!m_parser) |
| return { { }, { } }; |
| |
| OpcodeOrigin opcodeOrigin = OpcodeOrigin(m_parser->currentOpcode(), m_parser->currentOpcodeStartingOffset()); |
| switch (m_parser->currentOpcode()) { |
| case OpType::Ext1: |
| case OpType::ExtGC: |
| case OpType::ExtAtomic: |
| case OpType::ExtSIMD: |
| opcodeOrigin = OpcodeOrigin(m_parser->currentOpcode(), m_parser->currentExtendedOpcode(), m_parser->currentOpcodeStartingOffset()); |
| break; |
| default: |
| break; |
| } |
| ASSERT(isValidOpType(static_cast<uint8_t>(opcodeOrigin.opcode()))); |
| WasmOrigin result { CallSiteIndex(m_callSiteIndex), opcodeOrigin }; |
| if (m_context.origins.isEmpty() || m_context.origins.last() != result) |
| m_context.origins.append(result); |
| return m_context.origins.last(); |
| } |
| |
| ALWAYS_INLINE void BBQJIT::willParseOpcode() |
| { |
| OpType currentOpcode = m_parser->currentOpcode(); |
| switch (currentOpcode) { |
| case OpType::Ext1: |
| case OpType::ExtGC: |
| case OpType::ExtAtomic: |
| case OpType::ExtSIMD: |
| return; // We'll handle these once we know the extended opcode too. |
| default: |
| break; |
| } |
| |
| auto origin = this->origin(); |
| m_pcToCodeOriginMapBuilder.appendItem(m_jit.label(), CodeOrigin(BytecodeIndex(origin.m_opcodeOrigin.location()))); |
| if (m_disassembler) [[unlikely]] |
| m_disassembler->setOpcode(m_jit.label(), origin.m_opcodeOrigin); |
| |
| m_gprAllocator.assertAllValidRegistersAreUnlocked(); |
| m_fprAllocator.assertAllValidRegistersAreUnlocked(); |
| |
| #if ASSERT_ENABLED |
| if (shouldFuseBranchCompare && isCompareOpType(m_prevOpcode) |
| && (m_parser->currentOpcode() == OpType::BrIf || m_parser->currentOpcode() == OpType::If)) { |
| m_prevOpcode = m_parser->currentOpcode(); |
| return; |
| } |
| for (Value value : m_justPoppedStack) { |
| // Temps should have been consumed, we should have removed them from this list. |
| ASSERT_WITH_MESSAGE(!value.isTemp(), "Temp(%u) was not consumed by the instruction that popped it!", value.asTemp()); |
| |
| ASSERT(!value.isLocal()); // Change this if/when we start register-allocating locals. |
| } |
| m_prevOpcode = m_parser->currentOpcode(); |
| m_justPoppedStack.clear(); |
| #endif |
| } |
| |
| ALWAYS_INLINE void BBQJIT::willParseExtendedOpcode() |
| { |
| auto origin = this->origin(); |
| m_pcToCodeOriginMapBuilder.appendItem(m_jit.label(), CodeOrigin(BytecodeIndex(origin.m_opcodeOrigin.location()))); |
| if (m_disassembler) [[unlikely]] |
| m_disassembler->setOpcode(m_jit.label(), origin.m_opcodeOrigin); |
| |
| m_gprAllocator.assertAllValidRegistersAreUnlocked(); |
| m_fprAllocator.assertAllValidRegistersAreUnlocked(); |
| } |
| |
| ALWAYS_INLINE void BBQJIT::didParseOpcode() |
| { |
| } |
| |
| BBQJIT::BranchFoldResult BBQJIT::tryFoldFusedBranchCompare(OpType opType, ExpressionType operand) |
| { |
| if (!operand.isConst()) |
| return BranchNotFolded; |
| switch (opType) { |
| case OpType::I32Eqz: |
| return operand.asI32() ? BranchNeverTaken : BranchAlwaysTaken; |
| case OpType::I64Eqz: |
| return operand.asI64() ? BranchNeverTaken : BranchAlwaysTaken; |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Op type '%s' is not a unary comparison and should not have been fused.\n", makeString(opType).characters()); |
| } |
| return BranchNotFolded; |
| } |
| |
| BBQJIT::Jump BBQJIT::emitFusedBranchCompareBranch(OpType opType, ExpressionType, Location operandLocation) |
| { |
| // Emit the negation of the intended branch. |
| switch (opType) { |
| case OpType::I32Eqz: |
| return m_jit.branchTest32(ResultCondition::NonZero, operandLocation.asGPR()); |
| case OpType::I64Eqz: |
| #if USE(JSVALUE64) |
| return m_jit.branchTest64(ResultCondition::NonZero, operandLocation.asGPR()); |
| #else |
| return m_jit.branchTest64(ResultCondition::NonZero, operandLocation.asGPRhi(), operandLocation.asGPRlo()); |
| #endif |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Op type '%s' is not a unary comparison and should not have been fused.\n", makeString(opType).characters()); |
| } |
| } |
| |
| PartialResult BBQJIT::addFusedBranchCompare(OpType opType, ControlType& target, ExpressionType operand, Stack& results) |
| { |
| ASSERT(!operand.isNone()); |
| |
| switch (tryFoldFusedBranchCompare(opType, operand)) { |
| case BranchNeverTaken: |
| return { }; |
| case BranchAlwaysTaken: |
| currentControlData().flushAndSingleExit(*this, target, results, false, false); |
| target.addBranch(m_jit.jump()); |
| return { }; |
| case BranchNotFolded: |
| break; |
| } |
| |
| { |
| // Like in normal addBranch(), we can directly use the operand location |
| // because it shouldn't interfere with flushAtBlockBoundary(). |
| Location operandLocation = loadIfNecessary(operand); |
| consume(operand); |
| |
| LOG_INSTRUCTION("BranchCompare", makeString(opType).characters(), operand, operandLocation); |
| |
| currentControlData().flushAtBlockBoundary(*this, 0, results, false); |
| Jump ifNotTaken = emitFusedBranchCompareBranch(opType, operand, operandLocation); |
| currentControlData().addExit(*this, target.targetLocations(), results); |
| target.addBranch(m_jit.jump()); |
| ifNotTaken.link(&m_jit); |
| currentControlData().finalizeBlock(*this, target.targetLocations().size(), results, true); |
| } |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addFusedIfCompare(OpType op, ExpressionType operand, BlockSignature&& signature, Stack& enclosingStack, ControlData& result, Stack& newStack) |
| { |
| BranchFoldResult foldResult = tryFoldFusedBranchCompare(op, operand); |
| |
| ScratchScope<0, 1> scratches(*this); |
| Location operandLocation; |
| RegisterSet liveScratchGPRs, liveScratchFPRs; |
| if (foldResult == BranchNotFolded) { |
| if (!operand.isConst()) |
| operandLocation = loadIfNecessary(operand); |
| else if (operand.isFloat()) { |
| operandLocation = Location::fromFPR(scratches.fpr(0)); |
| emitMove(operand, operandLocation); |
| } |
| |
| if (operandLocation.isGPR()) |
| liveScratchGPRs.add(operandLocation.asGPR(), IgnoreVectors); |
| else if (operandLocation.isGPR2()) { |
| liveScratchGPRs.add(operandLocation.asGPRlo(), IgnoreVectors); |
| liveScratchGPRs.add(operandLocation.asGPRhi(), IgnoreVectors); |
| } else if (operandLocation.isFPR()) |
| liveScratchFPRs.add(operandLocation.asFPR(), operand.type() == TypeKind::V128 ? Width128 : Width64); |
| } |
| if (!liveScratchFPRs.contains(scratches.fpr(0), IgnoreVectors)) |
| scratches.unbindEarly(); |
| |
| consume(operand); |
| |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::If, WTF::move(signature), height, liveScratchGPRs, liveScratchFPRs); |
| |
| // Despite being conditional, if doesn't need to worry about diverging expression stacks at block boundaries, so it doesn't need multiple exits. |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("IfCompare", makeString(op).characters(), result.signature(), operand, operandLocation); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| |
| result.startBlock(*this, newStack); |
| if (foldResult == BranchNeverTaken) |
| result.setIfBranch(m_jit.jump()); // Emit direct branch if we know the condition is false. |
| else if (foldResult == BranchNotFolded) // Otherwise, we only emit a branch at all if we don't know the condition statically. |
| result.setIfBranch(emitFusedBranchCompareBranch(op, operand, operandLocation)); |
| return { }; |
| } |
| |
| BBQJIT::BranchFoldResult BBQJIT::tryFoldFusedBranchCompare(OpType opType, ExpressionType left, ExpressionType right) |
| { |
| if (!left.isConst() || !right.isConst()) |
| return BranchNotFolded; |
| switch (opType) { |
| case OpType::I32LtS: |
| return left.asI32() < right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32LtU: |
| return static_cast<uint32_t>(left.asI32()) < static_cast<uint32_t>(right.asI32()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32GtS: |
| return left.asI32() > right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32GtU: |
| return static_cast<uint32_t>(left.asI32()) > static_cast<uint32_t>(right.asI32()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32LeS: |
| return left.asI32() <= right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32LeU: |
| return static_cast<uint32_t>(left.asI32()) <= static_cast<uint32_t>(right.asI32()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32GeS: |
| return left.asI32() >= right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32GeU: |
| return static_cast<uint32_t>(left.asI32()) >= static_cast<uint32_t>(right.asI32()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32Eq: |
| return left.asI32() == right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I32Ne: |
| return left.asI32() != right.asI32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64LtS: |
| return left.asI64() < right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64LtU: |
| return static_cast<uint64_t>(left.asI64()) < static_cast<uint64_t>(right.asI64()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64GtS: |
| return left.asI64() > right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64GtU: |
| return static_cast<uint64_t>(left.asI64()) > static_cast<uint64_t>(right.asI64()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64LeS: |
| return left.asI64() <= right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64LeU: |
| return static_cast<uint64_t>(left.asI64()) <= static_cast<uint64_t>(right.asI64()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64GeS: |
| return left.asI64() >= right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64GeU: |
| return static_cast<uint64_t>(left.asI64()) >= static_cast<uint64_t>(right.asI64()) ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64Eq: |
| return left.asI64() == right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::I64Ne: |
| return left.asI64() != right.asI64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Lt: |
| return left.asF32() < right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Gt: |
| return left.asF32() > right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Le: |
| return left.asF32() <= right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Ge: |
| return left.asF32() >= right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Eq: |
| return left.asF32() == right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F32Ne: |
| return left.asF32() != right.asF32() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Lt: |
| return left.asF64() < right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Gt: |
| return left.asF64() > right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Le: |
| return left.asF64() <= right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Ge: |
| return left.asF64() >= right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Eq: |
| return left.asF64() == right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| case OpType::F64Ne: |
| return left.asF64() != right.asF64() ? BranchAlwaysTaken : BranchNeverTaken; |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Op type '%s' is not a binary comparison and should not have been fused.\n", makeString(opType).characters()); |
| } |
| } |
| |
| static MacroAssembler::Jump emitBranchI32(CCallHelpers& jit, MacroAssembler::RelationalCondition condition, Value left, Location leftLocation, Value right, Location rightLocation) |
| { |
| if (right.isConst()) |
| return jit.branch32(condition, leftLocation.asGPR(), MacroAssembler::TrustedImm32(right.asI32())); |
| if (left.isConst()) |
| return jit.branch32(condition, MacroAssembler::TrustedImm32(left.asI32()), rightLocation.asGPR()); |
| return jit.branch32(condition, leftLocation.asGPR(), rightLocation.asGPR()); |
| } |
| |
| static MacroAssembler::Jump emitBranchI64(CCallHelpers& jit, MacroAssembler::RelationalCondition condition, Value left, Location leftLocation, Value right, Location rightLocation) |
| { |
| #if USE(JSVALUE64) |
| if (right.isConst()) |
| return jit.branch64(condition, leftLocation.asGPR(), MacroAssembler::Imm64(right.asI64())); |
| if (left.isConst()) |
| return jit.branch64(MacroAssembler::commute(condition), rightLocation.asGPR(), MacroAssembler::Imm64(left.asI64())); |
| return jit.branch64(condition, leftLocation.asGPR(), rightLocation.asGPR()); |
| #else |
| if (right.isConst()) |
| return jit.branch64(condition, leftLocation.asGPRhi(), leftLocation.asGPRlo(), MacroAssembler::TrustedImm64(right.asI64())); |
| if (left.isConst()) |
| return jit.branch64(MacroAssembler::commute(condition), rightLocation.asGPRhi(), rightLocation.asGPRlo(), MacroAssembler::TrustedImm64(left.asI64())); |
| return jit.branch64(condition, leftLocation.asGPRhi(), leftLocation.asGPRlo(), rightLocation.asGPRhi(), rightLocation.asGPRlo()); |
| #endif |
| } |
| |
| static MacroAssembler::Jump emitBranchF32(CCallHelpers& jit, MacroAssembler::DoubleCondition condition, Value, Location leftLocation, Value, Location rightLocation) |
| { |
| return jit.branchFloat(condition, leftLocation.asFPR(), rightLocation.asFPR()); |
| } |
| |
| static MacroAssembler::Jump emitBranchF64(CCallHelpers& jit, MacroAssembler::DoubleCondition condition, Value, Location leftLocation, Value, Location rightLocation) |
| { |
| return jit.branchDouble(condition, leftLocation.asFPR(), rightLocation.asFPR()); |
| } |
| |
| BBQJIT::Jump BBQJIT::emitFusedBranchCompareBranch(OpType opType, ExpressionType left, Location leftLocation, ExpressionType right, Location rightLocation) |
| { |
| // Emit a branch with the inverse of the comparison. We're generating the "branch-if-false" case. |
| switch (opType) { |
| case OpType::I32LtS: |
| return emitBranchI32(m_jit, RelationalCondition::GreaterThanOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I32LtU: |
| return emitBranchI32(m_jit, RelationalCondition::AboveOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I32GtS: |
| return emitBranchI32(m_jit, RelationalCondition::LessThanOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I32GtU: |
| return emitBranchI32(m_jit, RelationalCondition::BelowOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I32LeS: |
| return emitBranchI32(m_jit, RelationalCondition::GreaterThan, left, leftLocation, right, rightLocation); |
| case OpType::I32LeU: |
| return emitBranchI32(m_jit, RelationalCondition::Above, left, leftLocation, right, rightLocation); |
| case OpType::I32GeS: |
| return emitBranchI32(m_jit, RelationalCondition::LessThan, left, leftLocation, right, rightLocation); |
| case OpType::I32GeU: |
| return emitBranchI32(m_jit, RelationalCondition::Below, left, leftLocation, right, rightLocation); |
| case OpType::I32Eq: |
| return emitBranchI32(m_jit, RelationalCondition::NotEqual, left, leftLocation, right, rightLocation); |
| case OpType::I32Ne: |
| return emitBranchI32(m_jit, RelationalCondition::Equal, left, leftLocation, right, rightLocation); |
| case OpType::I64LtS: |
| return emitBranchI64(m_jit, RelationalCondition::GreaterThanOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I64LtU: |
| return emitBranchI64(m_jit, RelationalCondition::AboveOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I64GtS: |
| return emitBranchI64(m_jit, RelationalCondition::LessThanOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I64GtU: |
| return emitBranchI64(m_jit, RelationalCondition::BelowOrEqual, left, leftLocation, right, rightLocation); |
| case OpType::I64LeS: |
| return emitBranchI64(m_jit, RelationalCondition::GreaterThan, left, leftLocation, right, rightLocation); |
| case OpType::I64LeU: |
| return emitBranchI64(m_jit, RelationalCondition::Above, left, leftLocation, right, rightLocation); |
| case OpType::I64GeS: |
| return emitBranchI64(m_jit, RelationalCondition::LessThan, left, leftLocation, right, rightLocation); |
| case OpType::I64GeU: |
| return emitBranchI64(m_jit, RelationalCondition::Below, left, leftLocation, right, rightLocation); |
| case OpType::I64Eq: |
| return emitBranchI64(m_jit, RelationalCondition::NotEqual, left, leftLocation, right, rightLocation); |
| case OpType::I64Ne: |
| return emitBranchI64(m_jit, RelationalCondition::Equal, left, leftLocation, right, rightLocation); |
| case OpType::F32Lt: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleLessThanAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F32Gt: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleGreaterThanAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F32Le: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleLessThanOrEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F32Ge: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleGreaterThanOrEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F32Eq: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F32Ne: |
| return emitBranchF32(m_jit, MacroAssembler::invert(DoubleCondition::DoubleNotEqualOrUnordered), left, leftLocation, right, rightLocation); |
| case OpType::F64Lt: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleLessThanAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F64Gt: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleGreaterThanAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F64Le: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleLessThanOrEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F64Ge: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleGreaterThanOrEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F64Eq: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleEqualAndOrdered), left, leftLocation, right, rightLocation); |
| case OpType::F64Ne: |
| return emitBranchF64(m_jit, MacroAssembler::invert(DoubleCondition::DoubleNotEqualOrUnordered), left, leftLocation, right, rightLocation); |
| default: |
| RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Op type '%s' is not a binary comparison and should not have been fused.\n", makeString(opType).characters()); |
| } |
| } |
| |
| PartialResult BBQJIT::addFusedBranchCompare(OpType opType, ControlType& target, ExpressionType left, ExpressionType right, Stack& results) |
| { |
| switch (tryFoldFusedBranchCompare(opType, left, right)) { |
| case BranchNeverTaken: |
| return { }; |
| case BranchAlwaysTaken: |
| currentControlData().flushAndSingleExit(*this, target, results, false, false); |
| target.addBranch(m_jit.jump()); |
| return { }; |
| case BranchNotFolded: |
| break; |
| } |
| |
| { |
| Location leftLocation, rightLocation; |
| |
| if (!left.isConst()) |
| leftLocation = loadIfNecessary(left); |
| else if (left.isFloat()) // Materialize floats here too, since they don't have a good immediate lowering. |
| emitMove(left, leftLocation = Location::fromFPR(wasmScratchFPR)); |
| if (!right.isConst()) |
| rightLocation = loadIfNecessary(right); |
| else if (right.isFloat()) |
| emitMove(right, rightLocation = Location::fromFPR(wasmScratchFPR)); |
| |
| consume(left); |
| consume(right); |
| |
| LOG_INSTRUCTION("BranchCompare", makeString(opType).characters(), left, leftLocation, right, rightLocation); |
| |
| currentControlData().flushAtBlockBoundary(*this, 0, results, false); |
| Jump ifNotTaken = emitFusedBranchCompareBranch(opType, left, leftLocation, right, rightLocation); |
| currentControlData().addExit(*this, target.targetLocations(), results); |
| target.addBranch(m_jit.jump()); |
| ifNotTaken.link(&m_jit); |
| currentControlData().finalizeBlock(*this, target.targetLocations().size(), results, true); |
| } |
| |
| return { }; |
| } |
| |
| [[nodiscard]] PartialResult BBQJIT::addFusedIfCompare(OpType op, ExpressionType left, ExpressionType right, BlockSignature&& signature, Stack& enclosingStack, ControlData& result, Stack& newStack) |
| { |
| BranchFoldResult foldResult = tryFoldFusedBranchCompare(op, left, right); |
| |
| Location leftLocation, rightLocation; |
| RegisterSet liveScratchGPRs, liveScratchFPRs; |
| if (foldResult == BranchNotFolded) { |
| ASSERT(!left.isConst() || !right.isConst()); // If they're both constants, we should have folded. |
| |
| if (!left.isConst()) |
| leftLocation = loadIfNecessary(left); |
| else if (left.isFloat()) |
| emitMove(left, leftLocation = Location::fromFPR(wasmScratchFPR)); |
| if (leftLocation.isGPR()) |
| liveScratchGPRs.add(leftLocation.asGPR(), IgnoreVectors); |
| else if (leftLocation.isGPR2()) { |
| liveScratchGPRs.add(leftLocation.asGPRlo(), IgnoreVectors); |
| liveScratchGPRs.add(leftLocation.asGPRhi(), IgnoreVectors); |
| } else if (leftLocation.isFPR()) |
| liveScratchFPRs.add(leftLocation.asFPR(), left.type() == TypeKind::V128 ? Width128 : Width64); |
| |
| if (!right.isConst()) |
| rightLocation = loadIfNecessary(right); |
| else if (right.isFloat()) |
| emitMove(right, rightLocation = Location::fromFPR(wasmScratchFPR)); |
| if (rightLocation.isGPR()) |
| liveScratchGPRs.add(rightLocation.asGPR(), IgnoreVectors); |
| else if (rightLocation.isGPR2()) { |
| liveScratchGPRs.add(rightLocation.asGPRlo(), IgnoreVectors); |
| liveScratchGPRs.add(rightLocation.asGPRhi(), IgnoreVectors); |
| } else if (rightLocation.isFPR()) |
| liveScratchFPRs.add(rightLocation.asFPR(), right.type() == TypeKind::V128 ? Width128 : Width64); |
| } |
| consume(left); |
| consume(right); |
| |
| auto height = currentControlData().enclosedHeight() + currentControlData().implicitSlots() + enclosingStack.size() - signature.argumentCount(); |
| result = ControlData(*this, BlockType::If, WTF::move(signature), height, liveScratchGPRs, liveScratchFPRs); |
| |
| // Despite being conditional, if doesn't need to worry about diverging expression stacks at block boundaries, so it doesn't need multiple exits. |
| currentControlData().flushAndSingleExit(*this, result, enclosingStack, true, false); |
| |
| LOG_INSTRUCTION("IfCompare", makeString(op).characters(), result.signature(), left, leftLocation, right, rightLocation); |
| LOG_INDENT(); |
| splitStack(result.signature(), enclosingStack, newStack); |
| |
| result.startBlock(*this, newStack); |
| if (foldResult == BranchNeverTaken) |
| result.setIfBranch(m_jit.jump()); // Emit direct branch if we know the condition is false. |
| else if (foldResult == BranchNotFolded) // Otherwise, we only emit a branch at all if we don't know the condition statically. |
| result.setIfBranch(emitFusedBranchCompareBranch(op, left, leftLocation, right, rightLocation)); |
| return { }; |
| } |
| |
| // SIMD |
| |
| bool BBQJIT::usesSIMD() |
| { |
| return m_usesSIMD; |
| } |
| |
| void BBQJIT::dump(const ControlStack&, const Stack*) { } |
| void BBQJIT::didFinishParsingLocals() { } |
| |
| void BBQJIT::didPopValueFromStack(Value value, ASCIILiteral) |
| { |
| UNUSED_PARAM(value); |
| #if ASSERT_ENABLED |
| m_justPoppedStack.append(value); |
| #endif |
| } |
| |
| void BBQJIT::finalize() |
| { |
| if (m_disassembler) [[unlikely]] |
| m_disassembler->setEndOfCode(m_jit.label()); |
| } |
| |
| Vector<UnlinkedHandlerInfo>&& BBQJIT::takeExceptionHandlers() |
| { |
| return WTF::move(m_exceptionHandlers); |
| } |
| |
| FixedBitVector&& BBQJIT::takeDirectCallees() |
| { |
| return WTF::move(m_directCallees); |
| } |
| |
| Vector<CCallHelpers::Label>&& BBQJIT::takeCatchEntrypoints() |
| { |
| return WTF::move(m_catchEntrypoints); |
| } |
| |
| Box<PCToCodeOriginMapBuilder> BBQJIT::takePCToCodeOriginMapBuilder() |
| { |
| if (m_pcToCodeOriginMapBuilder.didBuildMapping()) |
| return Box<PCToCodeOriginMapBuilder>::create(WTF::move(m_pcToCodeOriginMapBuilder)); |
| return nullptr; |
| } |
| |
| std::unique_ptr<BBQDisassembler> BBQJIT::takeDisassembler() |
| { |
| return WTF::move(m_disassembler); |
| } |
| |
| bool BBQJIT::isScratch(Location loc) |
| { |
| return (loc.isGPR() && loc.asGPR() == wasmScratchGPR) || (loc.isFPR() && loc.asFPR() == wasmScratchFPR); |
| } |
| |
| void BBQJIT::emitStore(Value src, Location dst) |
| { |
| if (src.isConst()) |
| return emitStoreConst(src, dst); |
| |
| LOG_INSTRUCTION("Store", src, RESULT(dst)); |
| emitStore(src.type(), locationOf(src), dst); |
| } |
| |
| void BBQJIT::emitMoveMemory(Value src, Location dst) |
| { |
| LOG_INSTRUCTION("Move", src, RESULT(dst)); |
| emitMoveMemory(src.type(), locationOf(src), dst); |
| } |
| |
| void BBQJIT::emitMoveRegister(Value src, Location dst) |
| { |
| if (!isScratch(locationOf(src)) && !isScratch(dst)) |
| LOG_INSTRUCTION("Move", src, RESULT(dst)); |
| |
| emitMoveRegister(src.type(), locationOf(src), dst); |
| } |
| |
| void BBQJIT::emitLoad(Value src, Location dst) |
| { |
| if (!isScratch(dst)) |
| LOG_INSTRUCTION("Load", src, RESULT(dst)); |
| |
| emitLoad(src.type(), locationOf(src), dst); |
| } |
| |
| void BBQJIT::emitMove(TypeKind type, Location src, Location dst) |
| { |
| if (src.isRegister()) { |
| if (dst.isMemory()) |
| emitStore(type, src, dst); |
| else |
| emitMoveRegister(type, src, dst); |
| } else { |
| if (dst.isMemory()) |
| emitMoveMemory(type, src, dst); |
| else |
| emitLoad(type, src, dst); |
| } |
| } |
| |
| void BBQJIT::emitMove(Value src, Location dst) |
| { |
| if (src.isConst()) { |
| if (dst.isMemory()) |
| emitStoreConst(src, dst); |
| else |
| emitMoveConst(src, dst); |
| } else { |
| Location srcLocation = locationOf(src); |
| emitMove(src.type(), srcLocation, dst); |
| } |
| } |
| |
| template<size_t N, typename OverflowHandler> |
| void BBQJIT::emitShuffle(Vector<Value, N, OverflowHandler>& srcVector, Vector<Location, N, OverflowHandler>& dstVector) |
| { |
| ASSERT(srcVector.size() == dstVector.size()); |
| |
| #if ASSERT_ENABLED |
| for (size_t i = 0; i < dstVector.size(); ++i) { |
| for (size_t j = i + 1; j < dstVector.size(); ++j) |
| ASSERT(dstVector[i] != dstVector[j]); |
| } |
| |
| // This algorithm assumes at most one cycle: https://xavierleroy.org/publi/parallel-move.pdf |
| for (size_t i = 0; i < srcVector.size(); ++i) { |
| for (size_t j = i + 1; j < srcVector.size(); ++j) { |
| ASSERT(srcVector[i].isConst() || srcVector[j].isConst() |
| || locationOf(srcVector[i]) != locationOf(srcVector[j])); |
| } |
| } |
| #endif |
| |
| if (srcVector.size() == 1) { |
| emitMove(srcVector[0], dstVector[0]); |
| return; |
| } |
| |
| // For multi-value return, a parallel move might be necessary. This is comparatively complex |
| // and slow, so we limit it to this slow path. |
| Vector<ShuffleStatus, N, OverflowHandler> statusVector(FillWith { }, srcVector.size(), ShuffleStatus::ToMove); |
| for (unsigned i = 0; i < srcVector.size(); i ++) { |
| if (statusVector[i] == ShuffleStatus::ToMove) |
| emitShuffleMove(srcVector, dstVector, statusVector, i); |
| } |
| } |
| |
| ControlData& BBQJIT::currentControlData() |
| { |
| return m_parser->controlStack().last().controlData; |
| } |
| |
| void BBQJIT::setLRUKey(Location location, LocalOrTempIndex key) |
| { |
| ASSERT(location.isRegister()); |
| if (location.isGPR()) { |
| m_gprAllocator.setSpillHint(location.asGPR(), key); |
| } else if (location.isFPR()) { |
| m_fprAllocator.setSpillHint(location.asFPR(), key); |
| } else if (location.isGPR2()) { |
| m_gprAllocator.setSpillHint(location.asGPRhi(), key); |
| m_gprAllocator.setSpillHint(location.asGPRlo(), key); |
| } |
| } |
| |
| void BBQJIT::increaseLRUKey(Location location) |
| { |
| setLRUKey(location, nextLRUKey()); |
| } |
| |
| Location BBQJIT::allocate(Value value) |
| { |
| return BBQJIT::allocateWithHint(value, Location::none()); |
| } |
| |
| Location BBQJIT::allocateWithHint(Value value, Location hint) |
| { |
| if (value.isPinned()) |
| return value.asPinned(); |
| |
| Location result; |
| if (value.isFloat()) |
| result = Location::fromFPR(m_fprAllocator.allocate(*this, RegisterBinding::fromValue(value), nextLRUKey(), hint.isFPR() ? hint.asFPR() : InvalidFPRReg)); |
| else if (!typeNeedsGPR2(value.type())) |
| result = Location::fromGPR(m_gprAllocator.allocate(*this, RegisterBinding::fromValue(value), nextLRUKey(), hint.isGPR() ? hint.asGPR() : InvalidGPRReg)); |
| else if constexpr (is32Bit()) { |
| // FIXME: Add hint support for GPR2s. |
| auto lruKey = nextLRUKey(); |
| GPRReg lo = m_gprAllocator.allocate(*this, RegisterBinding::fromValue(value), lruKey); |
| GPRReg hi = m_gprAllocator.allocate(*this, RegisterBinding::fromValue(value), lruKey); |
| result = Location::fromGPR2(hi, lo); |
| } else |
| RELEASE_ASSERT_NOT_REACHED(); |
| |
| if (value.isLocal()) |
| currentControlData().touch(value.asLocal()); |
| if (Options::verboseBBQJITAllocation()) [[unlikely]] |
| dataLogLn("BBQ\tAllocated ", value, " with type ", makeString(value.type()), " to ", result); |
| return bind(value, result); |
| } |
| |
| Location BBQJIT::locationOfWithoutBinding(Value value) |
| { |
| // Used internally in bind() to avoid infinite recursion. |
| if (value.isPinned()) |
| return value.asPinned(); |
| if (value.isLocal()) { |
| ASSERT(value.asLocal() < m_locals.size()); |
| return m_locals[value.asLocal()]; |
| } |
| if (value.isTemp()) { |
| if (value.asTemp() >= m_temps.size()) |
| return Location::none(); |
| return m_temps[value.asTemp()]; |
| } |
| return Location::none(); |
| } |
| |
| Location BBQJIT::locationOf(Value value) |
| { |
| if (value.isTemp()) { |
| if (value.asTemp() >= m_temps.size() || m_temps[value.asTemp()].isNone()) |
| bind(value, canonicalSlot(value)); |
| return m_temps[value.asTemp()]; |
| } |
| return locationOfWithoutBinding(value); |
| } |
| |
| Location BBQJIT::loadIfNecessary(Value value) |
| { |
| ASSERT(!value.isPinned()); // We should not load or move pinned values. |
| ASSERT(!value.isConst()); // We should not be materializing things we know are constants. |
| if (Options::verboseBBQJITAllocation()) [[unlikely]] |
| dataLogLn("BBQ\tLoading value ", value, " if necessary"); |
| Location loc = locationOf(value); |
| if (loc.isMemory()) { |
| if (Options::verboseBBQJITAllocation()) [[unlikely]] |
| dataLogLn("BBQ\tLoading local ", value, " to ", loc); |
| Location dst = allocate(value); // Find a register to store this value. Might spill older values if we run out. |
| emitLoad(value.type(), loc, dst); // Generate the load instructions to move the value into the register. |
| if (value.isLocal()) |
| currentControlData().touch(value.asLocal()); |
| loc = dst; |
| } else |
| increaseLRUKey(loc); |
| ASSERT(loc.isRegister()); |
| return loc; |
| } |
| |
| void BBQJIT::consume(Value value) |
| { |
| // Called whenever a value is popped from the expression stack. Mostly, we |
| // use this to release the registers temporaries are bound to. |
| Location location = locationOf(value); |
| if (value.isTemp() && location != canonicalSlot(value)) |
| unbind(value, location); |
| |
| #if ASSERT_ENABLED |
| m_justPoppedStack.removeFirstMatching([&](Value& popped) -> bool { |
| return popped.isTemp() && value.isTemp() && popped.asTemp() == value.asTemp(); |
| }); |
| #endif |
| } |
| |
| Location BBQJIT::bind(Value value, Location loc) |
| { |
| // Bind a value to a location. Doesn't touch the LRU, but updates the register set |
| // and local/temp tables accordingly. |
| |
| // Return early if we are already bound to the chosen location. Mostly this avoids |
| // spurious assertion failures. |
| Location currentLocation = locationOfWithoutBinding(value); |
| if (currentLocation == loc) |
| return currentLocation; |
| |
| // Technically, it could be possible to bind from a register to another register. But |
| // this risks (and is usually caused by) messing up the allocator state. So we check |
| // for it here. |
| ASSERT(!currentLocation.isRegister()); |
| |
| // Some callers have already updated the allocator to bind value to loc. But if not, we should do that bookkeeping now. |
| if (loc.isRegister()) { |
| if (value.isFloat()) { |
| if (m_fprAllocator.freeRegisters().contains(loc.asFPR(), Width::Width128)) |
| m_fprAllocator.bind(loc.asFPR(), RegisterBinding::fromValue(value), std::nullopt); |
| ASSERT(bindingFor(loc.asFPR()) == RegisterBinding::fromValue(value)); |
| } else if (loc.isGPR2()) { |
| if (m_gprAllocator.freeRegisters().contains(loc.asGPRlo(), IgnoreVectors)) |
| m_gprAllocator.bind(loc.asGPRlo(), RegisterBinding::fromValue(value), std::nullopt); |
| ASSERT(bindingFor(loc.asGPRlo()) == RegisterBinding::fromValue(value)); |
| if (m_gprAllocator.freeRegisters().contains(loc.asGPRhi(), IgnoreVectors)) |
| m_gprAllocator.bind(loc.asGPRhi(), RegisterBinding::fromValue(value), std::nullopt); |
| ASSERT(bindingFor(loc.asGPRhi()) == RegisterBinding::fromValue(value)); |
| } else { |
| if (m_gprAllocator.freeRegisters().contains(loc.asGPR(), IgnoreVectors)) |
| m_gprAllocator.bind(loc.asGPR(), RegisterBinding::fromValue(value), std::nullopt); |
| ASSERT(bindingFor(loc.asGPR()) == RegisterBinding::fromValue(value)); |
| } |
| } |
| if (value.isLocal()) |
| m_locals[value.asLocal()] = loc; |
| else if (value.isTemp()) { |
| if (m_temps.size() <= value.asTemp()) |
| m_temps.grow(value.asTemp() + 1); |
| m_temps[value.asTemp()] = loc; |
| } |
| |
| if (Options::verboseBBQJITAllocation()) [[unlikely]] |
| dataLogLn("BBQ\tBound value ", value, " to ", loc); |
| |
| return loc; |
| } |
| |
| void BBQJIT::unbind(Value value, Location loc) |
| { |
| // Unbind a value from a location. Doesn't touch the LRU, but updates the register set |
| // and local/temp tables accordingly. |
| if (loc.isFPR()) |
| m_fprAllocator.unbind(loc.asFPR()); |
| else if (loc.isGPR()) |
| m_gprAllocator.unbind(loc.asGPR()); |
| else if (loc.isGPR2()) { |
| m_gprAllocator.unbind(loc.asGPRlo()); |
| m_gprAllocator.unbind(loc.asGPRhi()); |
| } |
| if (value.isLocal()) |
| m_locals[value.asLocal()] = m_localSlots[value.asLocal()]; |
| else if (value.isTemp()) |
| m_temps[value.asTemp()] = Location::none(); |
| |
| if (Options::verboseBBQJITAllocation()) [[unlikely]] |
| dataLogLn("BBQ\tUnbound value ", value, " from ", loc); |
| } |
| |
| void BBQJIT::unbindAllRegisters() |
| { |
| auto doUnbind = [&](Reg reg) { |
| const auto& binding = bindingFor(reg); |
| Value value = binding.toValue(); |
| if (value.isNone()) |
| return; |
| |
| ASSERT(value.isTemp() || value.isLocal()); |
| unbind(value, locationOfWithoutBinding(value)); |
| }; |
| |
| for (auto reg : m_gprAllocator.validRegisters()) |
| doUnbind(reg); |
| |
| for (auto reg : m_fprAllocator.validRegisters()) |
| doUnbind(reg); |
| } |
| |
| Location BBQJIT::canonicalSlot(Value value) |
| { |
| ASSERT(value.isLocal() || value.isTemp()); |
| if (value.isLocal()) |
| return m_localSlots[value.asLocal()]; |
| |
| LocalOrTempIndex tempIndex = value.asTemp(); |
| uint32_t slotOffset = WTF::roundUpToMultipleOf<tempSlotSize>(m_localAndCalleeSaveStorage) + (tempIndex + 1) * tempSlotSize; |
| if (m_frameSizeForValidation < slotOffset) |
| m_frameSizeForValidation = slotOffset; |
| return Location::fromStack(-static_cast<int32_t>(slotOffset)); |
| } |
| |
| Location BBQJIT::allocateStack(Value value) |
| { |
| // Align stack for value size. |
| m_frameSizeForValidation = WTF::roundUpToMultipleOf(value.size(), m_frameSizeForValidation); |
| m_frameSizeForValidation += value.size(); |
| return Location::fromStack(-static_cast<int32_t>(m_frameSizeForValidation)); |
| } |
| |
| void BBQJIT::emitArrayGetPayload(StorageType type, GPRReg arrayGPR, GPRReg payloadGPR) |
| { |
| ASSERT(arrayGPR != payloadGPR); |
| if (JSWebAssemblyArray::needsV128AlignmentMask(type)) { |
| // V128 needs runtime masking: PreciseAllocation only guarantees 8-byte alignment. |
| m_jit.addPtr(MacroAssembler::TrustedImm32(JSWebAssemblyArray::offsetOfData() + 15), arrayGPR, payloadGPR); |
| m_jit.andPtr(MacroAssembler::TrustedImm32(-16), payloadGPR); |
| return; |
| } |
| // For all other types, alignment shift is a compile-time constant |
| // since JSCell always has >=8-byte alignment. |
| m_jit.addPtr(MacroAssembler::TrustedImm32(JSWebAssemblyArray::alignedOffsetOfData(type.elementSize())), arrayGPR, payloadGPR); |
| } |
| |
| } // namespace JSC::Wasm::BBQJITImpl |
| |
| Expected<std::unique_ptr<InternalFunction>, String> parseAndCompileBBQ(CompilationContext& compilationContext, IPIntCallee& profiledCallee, BBQCallee& callee, const FunctionData& function, const RTT& signature, Vector<UnlinkedWasmToWasmCall>& unlinkedWasmToWasmCalls, Module& module, CalleeGroup& calleeGroup, const ModuleInformation& info, MemoryMode mode, FunctionCodeIndex functionIndex) |
| { |
| CompilerTimingScope totalTime("BBQ"_s, "Total BBQ"_s); |
| |
| Thunks::singleton().stub(catchInWasmThunkGenerator); |
| |
| auto result = makeUnique<InternalFunction>(); |
| |
| compilationContext.wasmEntrypointJIT = makeUnique<CCallHelpers>(); |
| |
| BBQJIT irGenerator(compilationContext, signature, module, calleeGroup, profiledCallee, callee, function, functionIndex, info, unlinkedWasmToWasmCalls, mode, result.get()); |
| FunctionParser<BBQJIT> parser(irGenerator, function.data, signature, info); |
| WASM_FAIL_IF_HELPER_FAILS(parser.parse()); |
| |
| if (irGenerator.hasLoops()) |
| result->bbqSharedLoopEntrypoint = irGenerator.addLoopOSREntrypoint(); |
| |
| irGenerator.finalize(); |
| auto checkSize = irGenerator.stackCheckSize(); |
| if (!checkSize) |
| checkSize = stackCheckNotNeeded; |
| callee.setStackCheckSize(checkSize); |
| |
| result->exceptionHandlers = irGenerator.takeExceptionHandlers(); |
| result->outgoingJITDirectCallees = irGenerator.takeDirectCallees(); |
| compilationContext.catchEntrypoints = irGenerator.takeCatchEntrypoints(); |
| compilationContext.pcToCodeOriginMapBuilder = irGenerator.takePCToCodeOriginMapBuilder(); |
| compilationContext.bbqDisassembler = irGenerator.takeDisassembler(); |
| |
| return result; |
| } |
| |
| void BBQJIT::emitPushCalleeSaves() |
| { |
| size_t stackSizeForCalleeSaves = WTF::roundUpToMultipleOf<stackAlignmentBytes()>(RegisterAtOffsetList::bbqCalleeSaveRegisters().registerCount() * sizeof(UCPURegister)); |
| #if CPU(X86_64) || CPU(ARM64) |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(stackSizeForCalleeSaves), MacroAssembler::stackPointerRegister); |
| #else |
| m_jit.subPtr(GPRInfo::callFrameRegister, TrustedImm32(stackSizeForCalleeSaves), wasmScratchGPR); |
| m_jit.move(wasmScratchGPR, MacroAssembler::stackPointerRegister); |
| #endif |
| m_jit.emitSaveCalleeSavesFor(&RegisterAtOffsetList::bbqCalleeSaveRegisters()); |
| } |
| |
| void BBQJIT::emitRestoreCalleeSaves() |
| { |
| m_jit.emitRestoreCalleeSavesFor(&RegisterAtOffsetList::bbqCalleeSaveRegisters()); |
| } |
| |
| } } // namespace JSC::Wasm |
| |
| #endif // ENABLE(WEBASSEMBLY_OMGJIT) |