/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef jit_CacheIRCompiler_h #define jit_CacheIRCompiler_h #include "mozilla/Casting.h" #include "mozilla/Maybe.h" #include "jit/CacheIR.h" #include "jit/CacheIRReader.h" #include "jit/CacheIRWriter.h" #include "jit/JitOptions.h" #include "jit/MacroAssembler.h" #include "jit/PerfSpewer.h" #include "jit/SharedICRegisters.h" #include "js/ScalarType.h" // js::Scalar::Type namespace JS { class BigInt; } namespace js { class TypedArrayObject; enum class UnaryMathFunction : uint8_t; namespace jit { class BaselineCacheIRCompiler; class ICCacheIRStub; class IonCacheIRCompiler; class IonScript; enum class ICStubEngine : uint8_t; // [SMDOC] CacheIR Value Representation and Tracking // // While compiling an IC stub the CacheIR compiler needs to keep track of the // physical location for each logical piece of data we care about, as well as // ensure that in the case of a stub failing, we are able to restore the input // state so that a subsequent stub can attempt to provide a value. // // OperandIds are created in the CacheIR front-end to keep track of values that // are passed between CacheIR ops during the execution of a given CacheIR stub. // In the CacheRegisterAllocator these OperandIds are given OperandLocations, // that represent the physical location of the OperandId at a given point in // time during CacheRegister allocation. // // In the CacheRegisterAllocator physical locations include the stack, and // registers, as well as whether or not the value has been unboxed or not. // Constants are also represented separately to provide for on-demand // materialization. // // Intra-op Register allocation: // // During the emission of a CacheIR op, code can ask the CacheRegisterAllocator // for access to a particular OperandId, and the register allocator will // generate the required code to fill that request. // // Input OperandIds should be considered as immutable, and should not be mutated // during the execution of a stub. // // There are also a number of RAII classes that interact with the register // allocator, in order to provide access to more registers than just those // provided for by the OperandIds. // // - AutoOutputReg: The register which will hold the output value of the stub. // - AutoScratchReg: By default, an arbitrary scratch register, however a // specific register can be requested. // - AutoScratchRegMaybeOutput: Any arbitrary scratch register, but the output // register may be used as well. // // These RAII classes take ownership of a register for the duration of their // lifetime so they can be used for computation or output. The register // allocator can spill values with OperandLocations in order to try to ensure // that a register is made available for use. // // If a specific register is required (via AutoScratchRegister), it should be // the first register acquired, as the register rallocator will be unable to // allocate the fixed register if the current op is using it for something else. // // If no register can be provided after attempting to spill, a // MOZ_RELEASE_ASSERT ensures the browser will crash. The register allocator is // not provided enough information in its current design to insert spills and // fills at arbitrary locations, and so it can fail to find an allocation // solution. However, this will only happen within the implementation of an // operand emitter, and because the cache register allocator is mostly // determinstic, so long as the operand id emitter is tested, this won't // suddenly crop up in an arbitrary webpage. It's worth noting the most // difficult platform to support is x86-32, because it has the least number of // registers available. // // FailurePaths checkpoint the state of the register allocator so that the input // state can be recomputed from the current state before jumping to the next // stub in the IC chain. An important invariant is that the FailurePath must be // allocated for each op after all the manipulation of OperandLocations has // happened, so that its recording is correct. // // Inter-op Register Allocation: // // The RAII register management classes are RAII because all register state // outside the OperandLocations is reset before the compilation of each // individual CacheIR op. This means that you cannot rely on a value surviving // between ops, even if you use the ability of AutoScratchRegister to name a // specific register. Values that need to be preserved between ops must be given // an OperandId. // Represents a Value on the Baseline frame's expression stack. Slot 0 is the // value on top of the stack (the most recently pushed value), slot 1 is the // value pushed before that, etc. class BaselineFrameSlot { uint32_t slot_; public: explicit BaselineFrameSlot(uint32_t slot) : slot_(slot) {} uint32_t slot() const { return slot_; } bool operator==(const BaselineFrameSlot& other) const { return slot_ == other.slot_; } bool operator!=(const BaselineFrameSlot& other) const { return slot_ != other.slot_; } }; // OperandLocation represents the location of an OperandId. The operand is // either in a register or on the stack, and is either boxed or unboxed. class OperandLocation { public: enum Kind { Uninitialized = 0, PayloadReg, DoubleReg, ValueReg, PayloadStack, ValueStack, BaselineFrame, Constant, }; private: Kind kind_; union Data { struct { Register reg; JSValueType type; } payloadReg; FloatRegister doubleReg; ValueOperand valueReg; struct { uint32_t stackPushed; JSValueType type; } payloadStack; uint32_t valueStackPushed; BaselineFrameSlot baselineFrameSlot; Value constant; Data() : valueStackPushed(0) {} }; Data data_; public: OperandLocation() : kind_(Uninitialized) {} Kind kind() const { return kind_; } void setUninitialized() { kind_ = Uninitialized; } ValueOperand valueReg() const { MOZ_ASSERT(kind_ == ValueReg); return data_.valueReg; } Register payloadReg() const { MOZ_ASSERT(kind_ == PayloadReg); return data_.payloadReg.reg; } FloatRegister doubleReg() const { MOZ_ASSERT(kind_ == DoubleReg); return data_.doubleReg; } uint32_t payloadStack() const { MOZ_ASSERT(kind_ == PayloadStack); return data_.payloadStack.stackPushed; } uint32_t valueStack() const { MOZ_ASSERT(kind_ == ValueStack); return data_.valueStackPushed; } JSValueType payloadType() const { if (kind_ == PayloadReg) { return data_.payloadReg.type; } MOZ_ASSERT(kind_ == PayloadStack); return data_.payloadStack.type; } Value constant() const { MOZ_ASSERT(kind_ == Constant); return data_.constant; } BaselineFrameSlot baselineFrameSlot() const { MOZ_ASSERT(kind_ == BaselineFrame); return data_.baselineFrameSlot; } void setPayloadReg(Register reg, JSValueType type) { kind_ = PayloadReg; data_.payloadReg.reg = reg; data_.payloadReg.type = type; } void setDoubleReg(FloatRegister reg) { kind_ = DoubleReg; data_.doubleReg = reg; } void setValueReg(ValueOperand reg) { kind_ = ValueReg; data_.valueReg = reg; } void setPayloadStack(uint32_t stackPushed, JSValueType type) { kind_ = PayloadStack; data_.payloadStack.stackPushed = stackPushed; data_.payloadStack.type = type; } void setValueStack(uint32_t stackPushed) { kind_ = ValueStack; data_.valueStackPushed = stackPushed; } void setConstant(const Value& v) { kind_ = Constant; data_.constant = v; } void setBaselineFrame(BaselineFrameSlot slot) { kind_ = BaselineFrame; data_.baselineFrameSlot = slot; } bool isUninitialized() const { return kind_ == Uninitialized; } bool isInRegister() const { return kind_ == PayloadReg || kind_ == ValueReg; } bool isOnStack() const { return kind_ == PayloadStack || kind_ == ValueStack; } size_t stackPushed() const { if (kind_ == PayloadStack) { return data_.payloadStack.stackPushed; } MOZ_ASSERT(kind_ == ValueStack); return data_.valueStackPushed; } size_t stackSizeInBytes() const { if (kind_ == PayloadStack) { return sizeof(uintptr_t); } MOZ_ASSERT(kind_ == ValueStack); return sizeof(js::Value); } void adjustStackPushed(int32_t diff) { if (kind_ == PayloadStack) { data_.payloadStack.stackPushed += diff; return; } MOZ_ASSERT(kind_ == ValueStack); data_.valueStackPushed += diff; } bool aliasesReg(Register reg) const { if (kind_ == PayloadReg) { return payloadReg() == reg; } if (kind_ == ValueReg) { return valueReg().aliases(reg); } return false; } bool aliasesReg(ValueOperand reg) const { #if defined(JS_NUNBOX32) return aliasesReg(reg.typeReg()) || aliasesReg(reg.payloadReg()); #else return aliasesReg(reg.valueReg()); #endif } bool aliasesReg(const OperandLocation& other) const; bool operator==(const OperandLocation& other) const; bool operator!=(const OperandLocation& other) const { return !operator==(other); } }; struct SpilledRegister { Register reg; uint32_t stackPushed; SpilledRegister(Register reg, uint32_t stackPushed) : reg(reg), stackPushed(stackPushed) {} bool operator==(const SpilledRegister& other) const { return reg == other.reg && stackPushed == other.stackPushed; } bool operator!=(const SpilledRegister& other) const { return !(*this == other); } }; using SpilledRegisterVector = Vector; // Class to track and allocate registers while emitting IC code. class MOZ_RAII CacheRegisterAllocator { // The original location of the inputs to the cache. Vector origInputLocations_; // The current location of each operand. Vector operandLocations_; // Free lists for value- and payload-slots on stack Vector freeValueSlots_; Vector freePayloadSlots_; // The registers allocated while emitting the current CacheIR op. // This prevents us from allocating a register and then immediately // clobbering it for something else, while we're still holding on to it. LiveGeneralRegisterSet currentOpRegs_; const AllocatableGeneralRegisterSet allocatableRegs_; // Registers that are currently unused and available. AllocatableGeneralRegisterSet availableRegs_; // Registers that are available, but before use they must be saved and // then restored when returning from the stub. AllocatableGeneralRegisterSet availableRegsAfterSpill_; // Registers we took from availableRegsAfterSpill_ and spilled to the stack. SpilledRegisterVector spilledRegs_; // The number of bytes pushed on the native stack. uint32_t stackPushed_; #ifdef DEBUG // Flag used to assert individual CacheIR instructions don't allocate // registers after calling addFailurePath. bool addedFailurePath_; #endif // The index of the CacheIR instruction we're currently emitting. uint32_t currentInstruction_; // Whether the stack contains a double spilled by AutoScratchFloatRegister. bool hasAutoScratchFloatRegisterSpill_ = false; const CacheIRWriter& writer_; CacheRegisterAllocator(const CacheRegisterAllocator&) = delete; CacheRegisterAllocator& operator=(const CacheRegisterAllocator&) = delete; void freeDeadOperandLocations(MacroAssembler& masm); void spillOperandToStack(MacroAssembler& masm, OperandLocation* loc); void spillOperandToStackOrRegister(MacroAssembler& masm, OperandLocation* loc); void popPayload(MacroAssembler& masm, OperandLocation* loc, Register dest); void popValue(MacroAssembler& masm, OperandLocation* loc, ValueOperand dest); Address payloadAddress(MacroAssembler& masm, const OperandLocation* loc) const; Address valueAddress(MacroAssembler& masm, const OperandLocation* loc) const; #ifdef DEBUG void assertValidState() const; #endif public: friend class AutoScratchRegister; friend class AutoScratchRegisterExcluding; explicit CacheRegisterAllocator(const CacheIRWriter& writer) : allocatableRegs_(GeneralRegisterSet::All()), stackPushed_(0), #ifdef DEBUG addedFailurePath_(false), #endif currentInstruction_(0), writer_(writer) { } [[nodiscard]] bool init(); void initAvailableRegs(const AllocatableGeneralRegisterSet& available) { availableRegs_ = available; } void initAvailableRegsAfterSpill(); void fixupAliasedInputs(MacroAssembler& masm); OperandLocation operandLocation(size_t i) const { return operandLocations_[i]; } void setOperandLocation(size_t i, const OperandLocation& loc) { operandLocations_[i] = loc; } OperandLocation origInputLocation(size_t i) const { return origInputLocations_[i]; } void initInputLocation(size_t i, ValueOperand reg) { origInputLocations_[i].setValueReg(reg); operandLocations_[i].setValueReg(reg); } void initInputLocation(size_t i, Register reg, JSValueType type) { origInputLocations_[i].setPayloadReg(reg, type); operandLocations_[i].setPayloadReg(reg, type); } void initInputLocation(size_t i, FloatRegister reg) { origInputLocations_[i].setDoubleReg(reg); operandLocations_[i].setDoubleReg(reg); } void initInputLocation(size_t i, const Value& v) { origInputLocations_[i].setConstant(v); operandLocations_[i].setConstant(v); } void initInputLocation(size_t i, BaselineFrameSlot slot) { origInputLocations_[i].setBaselineFrame(slot); operandLocations_[i].setBaselineFrame(slot); } void initInputLocation(size_t i, const TypedOrValueRegister& reg); void initInputLocation(size_t i, const ConstantOrRegister& value); const SpilledRegisterVector& spilledRegs() const { return spilledRegs_; } [[nodiscard]] bool setSpilledRegs(const SpilledRegisterVector& regs) { spilledRegs_.clear(); return spilledRegs_.appendAll(regs); } bool hasAutoScratchFloatRegisterSpill() const { return hasAutoScratchFloatRegisterSpill_; } void setHasAutoScratchFloatRegisterSpill(bool b) { MOZ_ASSERT(hasAutoScratchFloatRegisterSpill_ != b); hasAutoScratchFloatRegisterSpill_ = b; } void nextOp() { #ifdef DEBUG assertValidState(); addedFailurePath_ = false; #endif currentOpRegs_.clear(); currentInstruction_++; } #ifdef DEBUG void setAddedFailurePath() { MOZ_ASSERT(!addedFailurePath_, "multiple failure paths for instruction"); addedFailurePath_ = true; } #endif bool isDeadAfterInstruction(OperandId opId) const { return writer_.operandIsDead(opId.id(), currentInstruction_ + 1); } uint32_t stackPushed() const { return stackPushed_; } void setStackPushed(uint32_t pushed) { stackPushed_ = pushed; } bool isAllocatable(Register reg) const { return allocatableRegs_.has(reg); } // Allocates a new register. Register allocateRegister(MacroAssembler& masm); ValueOperand allocateValueRegister(MacroAssembler& masm); void allocateFixedRegister(MacroAssembler& masm, Register reg); void allocateFixedValueRegister(MacroAssembler& masm, ValueOperand reg); // Releases a register so it can be reused later. void releaseRegister(Register reg) { MOZ_ASSERT(currentOpRegs_.has(reg)); availableRegs_.add(reg); currentOpRegs_.take(reg); } void releaseValueRegister(ValueOperand reg) { #ifdef JS_NUNBOX32 releaseRegister(reg.payloadReg()); releaseRegister(reg.typeReg()); #else releaseRegister(reg.valueReg()); #endif } // Removes spilled values from the native stack. This should only be // called after all registers have been allocated. void discardStack(MacroAssembler& masm); Address addressOf(MacroAssembler& masm, BaselineFrameSlot slot) const; BaseValueIndex addressOf(MacroAssembler& masm, Register argcReg, BaselineFrameSlot slot) const; // Returns the register for the given operand. If the operand is currently // not in a register, it will load it into one. ValueOperand useValueRegister(MacroAssembler& masm, ValOperandId val); Register useRegister(MacroAssembler& masm, TypedOperandId typedId); ConstantOrRegister useConstantOrRegister(MacroAssembler& masm, ValOperandId val); // Allocates an output register for the given operand. Register defineRegister(MacroAssembler& masm, TypedOperandId typedId); ValueOperand defineValueRegister(MacroAssembler& masm, ValOperandId val); // Loads (potentially coercing) and unboxes a value into a float register // This is infallible, as there should have been a previous guard // to ensure the value is already a number. // Does not change the allocator's state. void ensureDoubleRegister(MacroAssembler& masm, NumberOperandId op, FloatRegister dest) const; // Loads an unboxed value into a scratch register. This can be useful // especially on 32-bit x86 when there are not enough registers for // useRegister. // Does not change the allocator's state. void copyToScratchRegister(MacroAssembler& masm, TypedOperandId typedId, Register dest) const; void copyToScratchValueRegister(MacroAssembler& masm, ValOperandId valId, ValueOperand dest) const; // Returns |val|'s JSValueType or JSVAL_TYPE_UNKNOWN. JSValueType knownType(ValOperandId val) const; // Emits code to restore registers and stack to the state at the start of // the stub. void restoreInputState(MacroAssembler& masm, bool discardStack = true); // Returns the set of registers storing the IC input operands. GeneralRegisterSet inputRegisterSet() const; void saveIonLiveRegisters(MacroAssembler& masm, LiveRegisterSet liveRegs, Register scratch, IonScript* ionScript); void restoreIonLiveRegisters(MacroAssembler& masm, LiveRegisterSet liveRegs); }; // RAII class to allocate a scratch register and release it when we're done // with it. class MOZ_RAII AutoScratchRegister { CacheRegisterAllocator& alloc_; Register reg_; AutoScratchRegister(const AutoScratchRegister&) = delete; void operator=(const AutoScratchRegister&) = delete; public: AutoScratchRegister(CacheRegisterAllocator& alloc, MacroAssembler& masm, Register reg = InvalidReg) : alloc_(alloc) { if (reg != InvalidReg) { alloc.allocateFixedRegister(masm, reg); reg_ = reg; } else { reg_ = alloc.allocateRegister(masm); } MOZ_ASSERT(alloc_.currentOpRegs_.has(reg_)); } ~AutoScratchRegister() { alloc_.releaseRegister(reg_); } Register get() const { return reg_; } operator Register() const { return reg_; } }; // On x86, spectreBoundsCheck32 can emit better code if it has a scratch // register and index masking is enabled. class MOZ_RAII AutoSpectreBoundsScratchRegister { mozilla::Maybe scratch_; Register reg_ = InvalidReg; AutoSpectreBoundsScratchRegister(const AutoSpectreBoundsScratchRegister&) = delete; void operator=(const AutoSpectreBoundsScratchRegister&) = delete; public: AutoSpectreBoundsScratchRegister(CacheRegisterAllocator& alloc, MacroAssembler& masm) { #ifdef JS_CODEGEN_X86 if (JitOptions.spectreIndexMasking) { scratch_.emplace(alloc, masm); reg_ = scratch_->get(); } #endif } Register get() const { return reg_; } operator Register() const { return reg_; } }; // Scratch Register64. Implemented with a single AutoScratchRegister on 64-bit // platforms and two AutoScratchRegisters on 32-bit platforms. class MOZ_RAII AutoScratchRegister64 { AutoScratchRegister reg1_; #if JS_BITS_PER_WORD == 32 AutoScratchRegister reg2_; #endif public: AutoScratchRegister64(const AutoScratchRegister64&) = delete; void operator=(const AutoScratchRegister64&) = delete; #if JS_BITS_PER_WORD == 32 AutoScratchRegister64(CacheRegisterAllocator& alloc, MacroAssembler& masm) : reg1_(alloc, masm), reg2_(alloc, masm) {} Register64 get() const { return Register64(reg1_, reg2_); } #else AutoScratchRegister64(CacheRegisterAllocator& alloc, MacroAssembler& masm) : reg1_(alloc, masm) {} Register64 get() const { return Register64(reg1_); } #endif operator Register64() const { return get(); } }; // Scratch ValueOperand. Implemented with a single AutoScratchRegister on 64-bit // platforms and two AutoScratchRegisters on 32-bit platforms. class MOZ_RAII AutoScratchValueRegister { AutoScratchRegister reg1_; #if JS_BITS_PER_WORD == 32 AutoScratchRegister reg2_; #endif public: AutoScratchValueRegister(const AutoScratchValueRegister&) = delete; void operator=(const AutoScratchValueRegister&) = delete; #if JS_BITS_PER_WORD == 32 AutoScratchValueRegister(CacheRegisterAllocator& alloc, MacroAssembler& masm) : reg1_(alloc, masm), reg2_(alloc, masm) {} ValueOperand get() const { return ValueOperand(reg1_, reg2_); } #else AutoScratchValueRegister(CacheRegisterAllocator& alloc, MacroAssembler& masm) : reg1_(alloc, masm) {} ValueOperand get() const { return ValueOperand(reg1_); } #endif operator ValueOperand() const { return get(); } }; // The FailurePath class stores everything we need to generate a failure path // at the end of the IC code. The failure path restores the input registers, if // needed, and jumps to the next stub. class FailurePath { Vector inputs_; SpilledRegisterVector spilledRegs_; NonAssertingLabel label_; uint32_t stackPushed_; #ifdef DEBUG // Flag to ensure FailurePath::label() isn't taken while there's a scratch // float register which still needs to be restored. bool hasAutoScratchFloatRegister_ = false; #endif public: FailurePath() = default; FailurePath(FailurePath&& other) : inputs_(std::move(other.inputs_)), spilledRegs_(std::move(other.spilledRegs_)), label_(other.label_), stackPushed_(other.stackPushed_) {} Label* labelUnchecked() { return &label_; } Label* label() { MOZ_ASSERT(!hasAutoScratchFloatRegister_); return labelUnchecked(); } void setStackPushed(uint32_t i) { stackPushed_ = i; } uint32_t stackPushed() const { return stackPushed_; } [[nodiscard]] bool appendInput(const OperandLocation& loc) { return inputs_.append(loc); } OperandLocation input(size_t i) const { return inputs_[i]; } const SpilledRegisterVector& spilledRegs() const { return spilledRegs_; } [[nodiscard]] bool setSpilledRegs(const SpilledRegisterVector& regs) { MOZ_ASSERT(spilledRegs_.empty()); return spilledRegs_.appendAll(regs); } // If canShareFailurePath(other) returns true, the same machine code will // be emitted for two failure paths, so we can share them. bool canShareFailurePath(const FailurePath& other) const; void setHasAutoScratchFloatRegister() { #ifdef DEBUG MOZ_ASSERT(!hasAutoScratchFloatRegister_); hasAutoScratchFloatRegister_ = true; #endif } void clearHasAutoScratchFloatRegister() { #ifdef DEBUG MOZ_ASSERT(hasAutoScratchFloatRegister_); hasAutoScratchFloatRegister_ = false; #endif } }; /** * Wrap an offset so that a call can decide to embed a constant * or load from the stub data. */ class StubFieldOffset { private: uint32_t offset_; StubField::Type type_; public: StubFieldOffset(uint32_t offset, StubField::Type type) : offset_(offset), type_(type) {} uint32_t getOffset() { return offset_; } StubField::Type getStubFieldType() { return type_; } }; class AutoOutputRegister; // Base class for BaselineCacheIRCompiler and IonCacheIRCompiler. class MOZ_RAII CacheIRCompiler { protected: friend class AutoOutputRegister; friend class AutoStubFrame; friend class AutoSaveLiveRegisters; friend class AutoCallVM; friend class AutoScratchFloatRegister; friend class AutoAvailableFloatRegister; enum class Mode { Baseline, Ion }; bool enteredStubFrame_; bool isBaseline(); bool isIon(); BaselineCacheIRCompiler* asBaseline(); IonCacheIRCompiler* asIon(); JSContext* cx_; const CacheIRWriter& writer_; StackMacroAssembler masm; CacheRegisterAllocator allocator; Vector failurePaths; // Float registers that are live. Registers not in this set can be // clobbered and don't need to be saved before performing a VM call. // Doing this for non-float registers is a bit more complicated because // the IC register allocator allocates GPRs. LiveFloatRegisterSet liveFloatRegs_; mozilla::Maybe outputUnchecked_; Mode mode_; // Distance from the IC to the stub data; mostly will be // sizeof(stubType) uint32_t stubDataOffset_; enum class StubFieldPolicy { Address, Constant }; StubFieldPolicy stubFieldPolicy_; CacheIRCompiler(JSContext* cx, TempAllocator& alloc, const CacheIRWriter& writer, uint32_t stubDataOffset, Mode mode, StubFieldPolicy policy) : enteredStubFrame_(false), cx_(cx), writer_(writer), masm(cx, alloc), allocator(writer_), liveFloatRegs_(FloatRegisterSet::All()), mode_(mode), stubDataOffset_(stubDataOffset), stubFieldPolicy_(policy) { MOZ_ASSERT(!writer.failed()); } [[nodiscard]] bool addFailurePath(FailurePath** failure); [[nodiscard]] bool emitFailurePath(size_t i); // Returns the set of volatile float registers that are live. These // registers need to be saved when making non-GC calls with callWithABI. FloatRegisterSet liveVolatileFloatRegs() const { return FloatRegisterSet::Intersect(liveFloatRegs_.set(), FloatRegisterSet::Volatile()); } bool objectGuardNeedsSpectreMitigations(ObjOperandId objId) const { // Instructions like GuardShape need Spectre mitigations if // (1) mitigations are enabled and (2) the object is used by other // instructions (if the object is *not* used by other instructions, // zeroing its register is pointless). return JitOptions.spectreObjectMitigations && !allocator.isDeadAfterInstruction(objId); } private: void emitPostBarrierShared(Register obj, const ConstantOrRegister& val, Register scratch, Register maybeIndex); void emitPostBarrierShared(Register obj, ValueOperand val, Register scratch, Register maybeIndex) { emitPostBarrierShared(obj, ConstantOrRegister(val), scratch, maybeIndex); } protected: template void emitPostBarrierSlot(Register obj, const T& val, Register scratch) { emitPostBarrierShared(obj, val, scratch, InvalidReg); } template void emitPostBarrierElement(Register obj, const T& val, Register scratch, Register index) { MOZ_ASSERT(index != InvalidReg); emitPostBarrierShared(obj, val, scratch, index); } bool emitComparePointerResultShared(JSOp op, TypedOperandId lhsId, TypedOperandId rhsId); [[nodiscard]] bool emitMathFunctionNumberResultShared( UnaryMathFunction fun, FloatRegister inputScratch, ValueOperand output); template [[nodiscard]] bool emitBigIntBinaryOperationShared(BigIntOperandId lhsId, BigIntOperandId rhsId); template [[nodiscard]] bool emitBigIntUnaryOperationShared(BigIntOperandId inputId); bool emitDoubleIncDecResult(bool isInc, NumberOperandId inputId); using AtomicsReadWriteModifyFn = int32_t (*)(TypedArrayObject*, size_t, int32_t); [[nodiscard]] bool emitAtomicsReadModifyWriteResult( ObjOperandId objId, IntPtrOperandId indexId, uint32_t valueId, Scalar::Type elementType, AtomicsReadWriteModifyFn fn); using AtomicsReadWriteModify64Fn = JS::BigInt* (*)(JSContext*, TypedArrayObject*, size_t, const JS::BigInt*); template [[nodiscard]] bool emitAtomicsReadModifyWriteResult64(ObjOperandId objId, IntPtrOperandId indexId, uint32_t valueId); void emitActivateIterator(Register objBeingIterated, Register iterObject, Register nativeIter, Register scratch, Register scratch2, uint32_t enumeratorsAddrOffset); CACHE_IR_COMPILER_SHARED_GENERATED void emitLoadStubField(StubFieldOffset val, Register dest); void emitLoadStubFieldConstant(StubFieldOffset val, Register dest); void emitLoadValueStubField(StubFieldOffset val, ValueOperand dest); void emitLoadDoubleValueStubField(StubFieldOffset val, ValueOperand dest, FloatRegister scratch); uintptr_t readStubWord(uint32_t offset, StubField::Type type) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); MOZ_ASSERT((offset % sizeof(uintptr_t)) == 0); return writer_.readStubField(offset, type).asWord(); } uint64_t readStubInt64(uint32_t offset, StubField::Type type) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); MOZ_ASSERT((offset % sizeof(uintptr_t)) == 0); return writer_.readStubField(offset, type).asInt64(); } int32_t int32StubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return readStubWord(offset, StubField::Type::RawInt32); } uint32_t uint32StubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return readStubWord(offset, StubField::Type::RawInt32); } Shape* shapeStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (Shape*)readStubWord(offset, StubField::Type::Shape); } GetterSetter* getterSetterStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (GetterSetter*)readStubWord(offset, StubField::Type::GetterSetter); } JSObject* objectStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (JSObject*)readStubWord(offset, StubField::Type::JSObject); } Value valueStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); uint64_t raw = readStubInt64(offset, StubField::Type::Value); return Value::fromRawBits(raw); } double doubleStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); uint64_t raw = readStubInt64(offset, StubField::Type::Double); return mozilla::BitwiseCast(raw); } JSString* stringStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (JSString*)readStubWord(offset, StubField::Type::String); } JS::Symbol* symbolStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (JS::Symbol*)readStubWord(offset, StubField::Type::Symbol); } JS::Compartment* compartmentStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (JS::Compartment*)readStubWord(offset, StubField::Type::RawPointer); } BaseScript* baseScriptStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (BaseScript*)readStubWord(offset, StubField::Type::BaseScript); } const JSClass* classStubField(uintptr_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (const JSClass*)readStubWord(offset, StubField::Type::RawPointer); } const void* proxyHandlerStubField(uintptr_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (const void*)readStubWord(offset, StubField::Type::RawPointer); } const void* pointerStubField(uintptr_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return (const void*)readStubWord(offset, StubField::Type::RawPointer); } jsid idStubField(uint32_t offset) { MOZ_ASSERT(stubFieldPolicy_ == StubFieldPolicy::Constant); return jsid::fromRawBits(readStubWord(offset, StubField::Type::Id)); } #ifdef DEBUG void assertFloatRegisterAvailable(FloatRegister reg); #endif void callVMInternal(MacroAssembler& masm, VMFunctionId id); template void callVM(MacroAssembler& masm); }; // Ensures the IC's output register is available for writing. class MOZ_RAII AutoOutputRegister { TypedOrValueRegister output_; CacheRegisterAllocator& alloc_; AutoOutputRegister(const AutoOutputRegister&) = delete; void operator=(const AutoOutputRegister&) = delete; public: explicit AutoOutputRegister(CacheIRCompiler& compiler); ~AutoOutputRegister(); Register maybeReg() const { if (output_.hasValue()) { return output_.valueReg().scratchReg(); } if (!output_.typedReg().isFloat()) { return output_.typedReg().gpr(); } return InvalidReg; } bool hasValue() const { return output_.hasValue(); } ValueOperand valueReg() const { return output_.valueReg(); } AnyRegister typedReg() const { return output_.typedReg(); } JSValueType type() const { MOZ_ASSERT(!hasValue()); return ValueTypeFromMIRType(output_.type()); } operator TypedOrValueRegister() const { return output_; } }; enum class CallCanGC { CanGC, CanNotGC }; // Instructions that have to perform a callVM require a stub frame. Call its // enter() and leave() methods to enter/leave the stub frame. // Hoisted from jit/BaselineCacheIRCompiler.cpp. See there for method // definitions. class MOZ_RAII AutoStubFrame { BaselineCacheIRCompiler& compiler; #ifdef DEBUG uint32_t framePushedAtEnterStubFrame_; #endif AutoStubFrame(const AutoStubFrame&) = delete; void operator=(const AutoStubFrame&) = delete; public: explicit AutoStubFrame(BaselineCacheIRCompiler& compiler); void enter(MacroAssembler& masm, Register scratch, CallCanGC canGC = CallCanGC::CanGC); void leave(MacroAssembler& masm); #ifdef DEBUG ~AutoStubFrame(); #endif }; // AutoSaveLiveRegisters must be used when we make a call that can GC. The // constructor ensures all live registers are stored on the stack (where the GC // expects them) and the destructor restores these registers. class MOZ_RAII AutoSaveLiveRegisters { IonCacheIRCompiler& compiler_; AutoSaveLiveRegisters(const AutoSaveLiveRegisters&) = delete; void operator=(const AutoSaveLiveRegisters&) = delete; public: explicit AutoSaveLiveRegisters(IonCacheIRCompiler& compiler); ~AutoSaveLiveRegisters(); }; // Like AutoScratchRegister, but reuse a register of |output| if possible. class MOZ_RAII AutoScratchRegisterMaybeOutput { mozilla::Maybe scratch_; Register scratchReg_; AutoScratchRegisterMaybeOutput(const AutoScratchRegisterMaybeOutput&) = delete; void operator=(const AutoScratchRegisterMaybeOutput&) = delete; public: AutoScratchRegisterMaybeOutput(CacheRegisterAllocator& alloc, MacroAssembler& masm, const AutoOutputRegister& output) { scratchReg_ = output.maybeReg(); if (scratchReg_ == InvalidReg) { scratch_.emplace(alloc, masm); scratchReg_ = scratch_.ref(); } } AutoScratchRegisterMaybeOutput(CacheRegisterAllocator& alloc, MacroAssembler& masm) { scratch_.emplace(alloc, masm); scratchReg_ = scratch_.ref(); } Register get() const { return scratchReg_; } operator Register() const { return scratchReg_; } }; // Like AutoScratchRegisterMaybeOutput, but tries to use the ValueOperand's // type register for the scratch register on 32-bit. // // Word of warning: Passing an instance of this class and AutoOutputRegister to // functions may not work correctly, because no guarantee is given that the type // register is used last when modifying the output's ValueOperand. class MOZ_RAII AutoScratchRegisterMaybeOutputType { mozilla::Maybe scratch_; Register scratchReg_; public: AutoScratchRegisterMaybeOutputType(CacheRegisterAllocator& alloc, MacroAssembler& masm, const AutoOutputRegister& output) { #if defined(JS_NUNBOX32) scratchReg_ = output.hasValue() ? output.valueReg().typeReg() : InvalidReg; #else scratchReg_ = InvalidReg; #endif if (scratchReg_ == InvalidReg) { scratch_.emplace(alloc, masm); scratchReg_ = scratch_.ref(); } } AutoScratchRegisterMaybeOutputType( const AutoScratchRegisterMaybeOutputType&) = delete; void operator=(const AutoScratchRegisterMaybeOutputType&) = delete; Register get() const { return scratchReg_; } operator Register() const { return scratchReg_; } }; // AutoCallVM is a wrapper class that unifies methods shared by // IonCacheIRCompiler and BaselineCacheIRCompiler that perform a callVM, but // require stub specific functionality before performing the VM call. // // Expected Usage: // // OPs with implementations that may be unified by this class must: // - Be listed in the CACHEIR_OPS list but not in the CACHE_IR_SHARED_OPS // list // - Differ only in their use of `AutoSaveLiveRegisters`, // `AutoOutputRegister`, and `AutoScratchRegister`. The Ion // implementation will use `AutoSaveLiveRegisters` and // `AutoOutputRegister`, while the Baseline implementation will use // `AutoScratchRegister`. // - Both use the `callVM` method. // // Using AutoCallVM: // - The constructor initializes `AutoOutputRegister` for both compiler // types. Additionally it initializes an `AutoSaveLiveRegisters` for // CacheIRCompilers with the mode Ion, and initializes // `AutoScratchRegisterMaybeOutput` and `AutoStubFrame` variables for // compilers with mode Baseline. // - The `prepare()` method calls the IonCacheIRCompiler method // `prepareVMCall` for IonCacheIRCompilers, calls the `enter()` method of // `AutoStubFrame` for BaselineCacheIRCompilers, and calls the // `discardStack` method of the `Register` class for both compiler types. // - The `call()` method invokes `callVM` on the CacheIRCompiler and stores // the call result according to its type. Finally it calls the `leave` // method of `AutoStubFrame` for BaselineCacheIRCompilers. // // Expected Usage Example: // See: `CacheIRCompiler::emitCallGetSparseElementResult()` // // Restrictions: // - OPs that do not meet the criteria listed above can not be unified with // AutoCallVM // class MOZ_RAII AutoCallVM { MacroAssembler& masm_; CacheIRCompiler* compiler_; CacheRegisterAllocator& allocator_; mozilla::Maybe output_; // Baseline specific stuff mozilla::Maybe stubFrame_; mozilla::Maybe scratch_; // Ion specific stuff mozilla::Maybe save_; void storeResult(JSValueType returnType); template void storeResult(); void leaveBaselineStubFrame(); public: AutoCallVM(MacroAssembler& masm, CacheIRCompiler* compiler, CacheRegisterAllocator& allocator); void prepare(); template void call() { compiler_->callVM(masm_); storeResult(); leaveBaselineStubFrame(); } template void callNoResult() { compiler_->callVM(masm_); leaveBaselineStubFrame(); } const AutoOutputRegister& output() const { return *output_; } ValueOperand outputValueReg() const { return output_->valueReg(); } }; // RAII class to allocate FloatReg0 as a scratch register and release it when // we're done with it. The previous contents of FloatReg0 may be spilled on the // stack and, if necessary, are restored when the destructor runs. // // When FailurePath is passed to the constructor, FailurePath::label() must not // be used during the life time of the AutoScratchFloatRegister. Instead use // AutoScratchFloatRegister::failure(). class MOZ_RAII AutoScratchFloatRegister { Label failurePopReg_{}; CacheIRCompiler* compiler_; FailurePath* failure_; AutoScratchFloatRegister(const AutoScratchFloatRegister&) = delete; void operator=(const AutoScratchFloatRegister&) = delete; public: explicit AutoScratchFloatRegister(CacheIRCompiler* compiler) : AutoScratchFloatRegister(compiler, nullptr) {} AutoScratchFloatRegister(CacheIRCompiler* compiler, FailurePath* failure); ~AutoScratchFloatRegister(); Label* failure(); FloatRegister get() const { return FloatReg0; } operator FloatRegister() const { return FloatReg0; } }; // This class can be used to assert a certain FloatRegister is available. In // Baseline mode, all float registers are available. In Ion mode, only the // registers added as fixed temps in LIRGenerator are available. class MOZ_RAII AutoAvailableFloatRegister { FloatRegister reg_; AutoAvailableFloatRegister(const AutoAvailableFloatRegister&) = delete; void operator=(const AutoAvailableFloatRegister&) = delete; public: explicit AutoAvailableFloatRegister(CacheIRCompiler& compiler, FloatRegister reg) : reg_(reg) { #ifdef DEBUG compiler.assertFloatRegisterAvailable(reg); #endif } FloatRegister get() const { return reg_; } operator FloatRegister() const { return reg_; } }; // See the 'Sharing Baseline stub code' comment in CacheIR.h for a description // of this class. // // CacheIRStubInfo has a trailing variable-length array of bytes. The memory // layout is as follows: // // Item | Offset // -----------------+-------------------------------------- // CacheIRStubInfo | 0 // CacheIR bytecode | sizeof(CacheIRStubInfo) // Stub field types | sizeof(CacheIRStubInfo) + codeLength_ // // The array of stub field types is terminated by StubField::Type::Limit. class CacheIRStubInfo { uint32_t codeLength_; CacheKind kind_; ICStubEngine engine_; uint8_t stubDataOffset_; bool makesGCCalls_; CacheIRStubInfo(CacheKind kind, ICStubEngine engine, bool makesGCCalls, uint32_t stubDataOffset, uint32_t codeLength) : codeLength_(codeLength), kind_(kind), engine_(engine), stubDataOffset_(stubDataOffset), makesGCCalls_(makesGCCalls) { MOZ_ASSERT(kind_ == kind, "Kind must fit in bitfield"); MOZ_ASSERT(engine_ == engine, "Engine must fit in bitfield"); MOZ_ASSERT(stubDataOffset_ == stubDataOffset, "stubDataOffset must fit in uint8_t"); } CacheIRStubInfo(const CacheIRStubInfo&) = delete; CacheIRStubInfo& operator=(const CacheIRStubInfo&) = delete; public: CacheKind kind() const { return kind_; } ICStubEngine engine() const { return engine_; } bool makesGCCalls() const { return makesGCCalls_; } const uint8_t* code() const { return reinterpret_cast(this) + sizeof(CacheIRStubInfo); } uint32_t codeLength() const { return codeLength_; } uint32_t stubDataOffset() const { return stubDataOffset_; } size_t stubDataSize() const; StubField::Type fieldType(uint32_t i) const { static_assert(sizeof(StubField::Type) == sizeof(uint8_t)); const uint8_t* fieldTypes = code() + codeLength_; return static_cast(fieldTypes[i]); } static CacheIRStubInfo* New(CacheKind kind, ICStubEngine engine, bool canMakeCalls, uint32_t stubDataOffset, const CacheIRWriter& writer); template js::GCPtr& getStubField(Stub* stub, uint32_t offset) const; template T* getPtrStubField(Stub* stub, uint32_t offset) const; template js::GCPtr& getStubField(ICCacheIRStub* stub, uint32_t offset) const { return getStubField(stub, offset); } uintptr_t getStubRawWord(const uint8_t* stubData, uint32_t offset) const; uintptr_t getStubRawWord(ICCacheIRStub* stub, uint32_t offset) const; int64_t getStubRawInt64(const uint8_t* stubData, uint32_t offset) const; int64_t getStubRawInt64(ICCacheIRStub* stub, uint32_t offset) const; void replaceStubRawWord(uint8_t* stubData, uint32_t offset, uintptr_t oldWord, uintptr_t newWord) const; }; template void TraceCacheIRStub(JSTracer* trc, T* stub, const CacheIRStubInfo* stubInfo); } // namespace jit } // namespace js #endif /* jit_CacheIRCompiler_h */