Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
*
* Copyright 2016 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* [SMDOC] WebAssembly baseline compiler (RabaldrMonkey)
*
* For now, see WasmBCClass.h for general comments about the compiler's
* structure.
*
* ----------------
*
* General assumptions for 32-bit vs 64-bit code:
*
* - A 32-bit register can be extended in-place to a 64-bit register on 64-bit
* systems.
*
* - Code that knows that Register64 has a '.reg' member on 64-bit systems and
* '.high' and '.low' members on 32-bit systems, or knows the implications
* thereof, is #ifdef JS_PUNBOX64. All other code is #if(n)?def JS_64BIT.
*
* Coding standards are a little fluid:
*
* - In "small" code generating functions (eg emitMultiplyF64, emitQuotientI32,
* and surrounding functions; most functions fall into this class) where the
* meaning is obvious:
*
* Old school:
* - if there is a single source + destination register, it is called 'r'
* - if there is one source and a different destination, they are called 'rs'
* and 'rd'
* - if there is one source + destination register and another source register
* they are called 'r' and 'rs'
* - if there are two source registers and a destination register they are
* called 'rs0', 'rs1', and 'rd'.
*
* The new thing:
* - what is called 'r' in the old-school naming scheme is increasingly called
* 'rsd' in source+dest cases.
*
* - Generic temp registers are named /temp[0-9]?/ not /tmp[0-9]?/.
*
* - Registers can be named non-generically for their function ('rp' for the
* 'pointer' register and 'rv' for the 'value' register are typical) and those
* names may or may not have an 'r' prefix.
*
* - "Larger" code generating functions make their own rules.
*/
/*
* [SMDOC] WebAssembly baseline compiler -- Lazy Tier-Up mechanism
*
* For baseline functions, we compile in code to monitor the function's
* "hotness" and request tier-up once that hotness crosses a threshold.
*
* (1) Each function has an associated int32_t counter,
* FuncDefInstanceData::hotnessCounter. These are stored in an array in
* the Instance. Hence access to them is fast and thread-local.
*
* (2) On instantiation, the counters are set to some positive number
* (Instance::init, Instance::computeInitialHotnessCounter), which is a
* very crude estimate of the cost of Ion compilation of the function.
*
* (3) In baseline compilation, a function decrements its counter at every
* entry (BaseCompiler::beginFunction) and at the start of every loop
* iteration (BaseCompiler::emitLoop). The decrement code is created by
* BaseCompiler::addHotnessCheck.
*
* (4) The decrement is by some value in the range 1 .. 127, as computed from
* the function or loop-body size, by BlockSizeToDownwardsStep.
*
* (5) For loops, the body size is known only at the end of the loop, but the
* check is required at the start of the body. Hence the value is patched
* in at the end (BaseCompiler::emitEnd, case LabelKind::Loop).
*
* (6) BaseCompiler::addHotnessCheck creates the shortest possible
* decrement/check code, to minimise both time and code-space overhead. On
* Intel it is only two instructions. The counter has the value from (4)
* subtracted from it. If the result is negative, we jump to OOL code
* (class OutOfLineRequestTierUp) which requests tier up; control then
* continues immediately after the check.
*
* (7) The OOL tier-up request code calls the stub pointed to by
* Instance::requestTierUpStub_. This always points to the stub created by
* GenerateRequestTierUpStub. This saves all registers and calls onwards
* to WasmHandleRequestTierUp in C++-land.
*
* (8) WasmHandleRequestTierUp figures out which function in which Instance is
* requesting tier-up. It sets the function's counter (1) to the largest
* possible value, which is 2^31-1. It then calls onwards to
* Code::requestTierUp, which requests off-thread Ion compilation of the
* function, then immediately returns.
*
* (9) It is important that (8) sets the counter to 2^31-1 (as close to
* infinity as possible). This is because it may be arbitrarily long
* before the optimised code becomes available. In the meantime the
* baseline version of the function will continue to run. We do not want
* it to make frequent duplicate requests for tier-up. Although a request
* for tier-up is relatively cheap (a few hundred instructions), it is
* still way more expensive than the fast-case for a hotness check (2 insns
* on Intel), and performance of the baseline code will be badly affected
* if it makes many duplicate requests.
*
* (10) Of course it is impossible to *guarantee* that a baseline function will
* not make a duplicate request, because the Ion compilation of the
* function could take arbitrarily long, or even fail completely (eg OOM).
* Hence it is necessary for WasmCode::requestTierUp (8) to detect and
* ignore duplicate requests.
*
* (11) Each Instance of a Module runs in its own thread and has its own array
* of counters. This makes the counter updating thread-local and cheap.
* But it means that, if a Module has multiple threads (Instances), it
* could be that a function never gets hot enough to request tier up,
* because it is not hot enough in any single thread, even though the
* total hotness summed across all threads is enough to request tier up.
* Whether this inaccuracy is a problem in practice remains to be seen.
*
* (12) Code::requestTierUp (8) creates a PartialTier2CompileTask and queues it
* for execution. It does not do the compilation itself.
*
* (13) A PartialTier2CompileTask's runHelperThreadTask (running on a helper
* thread) calls CompilePartialTier2. This compiles the function with Ion
* and racily updates the tiering table entry for the function, which
* lives in Code::jumpTables_::tiering_.
*
* (14) Subsequent calls to the function's baseline entry points will then jump
* to the Ion version of the function. Hence lazy tier-up is achieved.
*/
#include "wasm/WasmBaselineCompile.h"
#include "wasm/WasmAnyRef.h"
#include "wasm/WasmBCClass.h"
#include "wasm/WasmBCDefs.h"
#include "wasm/WasmBCFrame.h"
#include "wasm/WasmBCRegDefs.h"
#include "wasm/WasmBCStk.h"
#include "wasm/WasmValType.h"
#include "jit/MacroAssembler-inl.h"
#include "wasm/WasmBCClass-inl.h"
#include "wasm/WasmBCCodegen-inl.h"
#include "wasm/WasmBCRegDefs-inl.h"
#include "wasm/WasmBCRegMgmt-inl.h"
#include "wasm/WasmBCStkMgmt-inl.h"
namespace js {
namespace wasm {
using namespace js::jit;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;
////////////////////////////////////////////////////////////
//
// Out of line code management.
// The baseline compiler will use OOL code more sparingly than Ion since our
// code is not high performance and frills like code density and branch
// prediction friendliness will be less important.
class OutOfLineCode : public TempObject {
private:
NonAssertingLabel entry_;
NonAssertingLabel rejoin_;
StackHeight stackHeight_;
public:
OutOfLineCode() : stackHeight_(StackHeight::Invalid()) {}
Label* entry() { return &entry_; }
Label* rejoin() { return &rejoin_; }
void setStackHeight(StackHeight stackHeight) {
MOZ_ASSERT(!stackHeight_.isValid());
stackHeight_ = stackHeight;
}
void bind(BaseStackFrame* fr, MacroAssembler* masm) {
MOZ_ASSERT(stackHeight_.isValid());
masm->bind(&entry_);
fr->setStackHeight(stackHeight_);
}
// The generate() method must be careful about register use because it will be
// invoked when there is a register assignment in the BaseCompiler that does
// not correspond to the available registers when the generated OOL code is
// executed. The register allocator *must not* be called.
//
// The best strategy is for the creator of the OOL object to allocate all
// temps that the OOL code will need.
//
// Input, output, and temp registers are embedded in the OOL object and are
// known to the code generator.
//
// Scratch registers are available to use in OOL code.
//
// All other registers must be explicitly saved and restored by the OOL code
// before being used.
virtual void generate(MacroAssembler* masm) = 0;
};
OutOfLineCode* BaseCompiler::addOutOfLineCode(OutOfLineCode* ool) {
if (!ool || !outOfLine_.append(ool)) {
return nullptr;
}
ool->setStackHeight(fr.stackHeight());
return ool;
}
bool BaseCompiler::generateOutOfLineCode() {
for (auto* ool : outOfLine_) {
if (!ool->entry()->used()) {
continue;
}
ool->bind(&fr, &masm);
ool->generate(&masm);
}
return !masm.oom();
}
//////////////////////////////////////////////////////////////////////////////
//
// Sundry code generation.
bool BaseCompiler::addInterruptCheck() {
#ifdef RABALDR_PIN_INSTANCE
Register tmp(InstanceReg);
#else
ScratchI32 tmp(*this);
fr.loadInstancePtr(tmp);
#endif
Label ok;
masm.branch32(Assembler::Equal,
Address(tmp, wasm::Instance::offsetOfInterrupt()), Imm32(0),
&ok);
trap(wasm::Trap::CheckInterrupt);
masm.bind(&ok);
return createStackMap("addInterruptCheck");
}
void BaseCompiler::checkDivideByZero(RegI32 rhs) {
Label nonZero;
masm.branchTest32(Assembler::NonZero, rhs, rhs, &nonZero);
trap(Trap::IntegerDivideByZero);
masm.bind(&nonZero);
}
void BaseCompiler::checkDivideByZero(RegI64 r) {
Label nonZero;
ScratchI32 scratch(*this);
masm.branchTest64(Assembler::NonZero, r, r, scratch, &nonZero);
trap(Trap::IntegerDivideByZero);
masm.bind(&nonZero);
}
void BaseCompiler::checkDivideSignedOverflow(RegI32 rhs, RegI32 srcDest,
Label* done, bool zeroOnOverflow) {
Label notMin;
masm.branch32(Assembler::NotEqual, srcDest, Imm32(INT32_MIN), &notMin);
if (zeroOnOverflow) {
masm.branch32(Assembler::NotEqual, rhs, Imm32(-1), &notMin);
moveImm32(0, srcDest);
masm.jump(done);
} else {
masm.branch32(Assembler::NotEqual, rhs, Imm32(-1), &notMin);
trap(Trap::IntegerOverflow);
}
masm.bind(&notMin);
}
void BaseCompiler::checkDivideSignedOverflow(RegI64 rhs, RegI64 srcDest,
Label* done, bool zeroOnOverflow) {
Label notmin;
masm.branch64(Assembler::NotEqual, srcDest, Imm64(INT64_MIN), &notmin);
masm.branch64(Assembler::NotEqual, rhs, Imm64(-1), &notmin);
if (zeroOnOverflow) {
masm.xor64(srcDest, srcDest);
masm.jump(done);
} else {
trap(Trap::IntegerOverflow);
}
masm.bind(&notmin);
}
void BaseCompiler::jumpTable(const LabelVector& labels, Label* theTable) {
// Flush constant pools to ensure that the table is never interrupted by
// constant pool entries.
masm.flush();
#if defined(JS_CODEGEN_ARM) || defined(JS_CODEGEN_ARM64)
// Prevent nop sequences to appear in the jump table.
AutoForbidNops afn(&masm);
#endif
masm.bind(theTable);
for (const auto& label : labels) {
CodeLabel cl;
masm.writeCodePointer(&cl);
cl.target()->bind(label.offset());
masm.addCodeLabel(cl);
}
}
void BaseCompiler::tableSwitch(Label* theTable, RegI32 switchValue,
Label* dispatchCode) {
masm.bind(dispatchCode);
#if defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_X86)
ScratchI32 scratch(*this);
CodeLabel tableCl;
masm.mov(&tableCl, scratch);
tableCl.target()->bind(theTable->offset());
masm.addCodeLabel(tableCl);
masm.jmp(Operand(scratch, switchValue, ScalePointer));
#elif defined(JS_CODEGEN_ARM)
// Flush constant pools: offset must reflect the distance from the MOV
// to the start of the table; as the address of the MOV is given by the
// label, nothing must come between the bind() and the ma_mov().
AutoForbidPoolsAndNops afp(&masm,
/* number of instructions in scope = */ 5);
ScratchI32 scratch(*this);
// Compute the offset from the ma_mov instruction to the jump table.
Label here;
masm.bind(&here);
uint32_t offset = here.offset() - theTable->offset();
// Read PC+8
masm.ma_mov(pc, scratch);
// ARM scratch register is required by ma_sub.
ScratchRegisterScope arm_scratch(*this);
// Compute the absolute table base pointer into `scratch`, offset by 8
// to account for the fact that ma_mov read PC+8.
masm.ma_sub(Imm32(offset + 8), scratch, arm_scratch);
// Jump indirect via table element.
masm.ma_ldr(DTRAddr(scratch, DtrRegImmShift(switchValue, LSL, 2)), pc, Offset,
Assembler::Always);
#elif defined(JS_CODEGEN_MIPS64) || defined(JS_CODEGEN_LOONG64) || \
defined(JS_CODEGEN_RISCV64)
ScratchI32 scratch(*this);
CodeLabel tableCl;
masm.ma_li(scratch, &tableCl);
tableCl.target()->bind(theTable->offset());
masm.addCodeLabel(tableCl);
masm.branchToComputedAddress(BaseIndex(scratch, switchValue, ScalePointer));
#elif defined(JS_CODEGEN_ARM64)
AutoForbidPoolsAndNops afp(&masm,
/* number of instructions in scope = */ 4);
ScratchI32 scratch(*this);
ARMRegister s(scratch, 64);
ARMRegister v(switchValue, 64);
masm.Adr(s, theTable);
masm.Add(s, s, Operand(v, vixl::LSL, 3));
masm.Ldr(s, MemOperand(s, 0));
masm.Br(s);
#else
MOZ_CRASH("BaseCompiler platform hook: tableSwitch");
#endif
}
// Helpers for accessing the "baseline scratch" areas: all targets
void BaseCompiler::stashWord(RegPtr instancePtr, size_t index, RegPtr r) {
MOZ_ASSERT(r != instancePtr);
MOZ_ASSERT(index < Instance::N_BASELINE_SCRATCH_WORDS);
masm.storePtr(r,
Address(instancePtr, Instance::offsetOfBaselineScratchWords() +
index * sizeof(uintptr_t)));
}
void BaseCompiler::unstashWord(RegPtr instancePtr, size_t index, RegPtr r) {
MOZ_ASSERT(index < Instance::N_BASELINE_SCRATCH_WORDS);
masm.loadPtr(Address(instancePtr, Instance::offsetOfBaselineScratchWords() +
index * sizeof(uintptr_t)),
r);
}
// Helpers for accessing the "baseline scratch" areas: X86 only
#ifdef JS_CODEGEN_X86
void BaseCompiler::stashI64(RegPtr regForInstance, RegI64 r) {
static_assert(sizeof(uintptr_t) == 4);
MOZ_ASSERT(Instance::sizeOfBaselineScratchWords() >= 8);
MOZ_ASSERT(regForInstance != r.low && regForInstance != r.high);
# ifdef RABALDR_PIN_INSTANCE
# error "Pinned instance not expected"
# endif
fr.loadInstancePtr(regForInstance);
masm.store32(
r.low, Address(regForInstance, Instance::offsetOfBaselineScratchWords()));
masm.store32(r.high, Address(regForInstance,
Instance::offsetOfBaselineScratchWords() + 4));
}
void BaseCompiler::unstashI64(RegPtr regForInstance, RegI64 r) {
static_assert(sizeof(uintptr_t) == 4);
MOZ_ASSERT(Instance::sizeOfBaselineScratchWords() >= 8);
# ifdef RABALDR_PIN_INSTANCE
# error "Pinned instance not expected"
# endif
fr.loadInstancePtr(regForInstance);
if (regForInstance == r.low) {
masm.load32(
Address(regForInstance, Instance::offsetOfBaselineScratchWords() + 4),
r.high);
masm.load32(
Address(regForInstance, Instance::offsetOfBaselineScratchWords()),
r.low);
} else {
masm.load32(
Address(regForInstance, Instance::offsetOfBaselineScratchWords()),
r.low);
masm.load32(
Address(regForInstance, Instance::offsetOfBaselineScratchWords() + 4),
r.high);
}
}
#endif
// Given the bytecode size of a block (a complete function body, or a loop
// body), return the required downwards step for the associated hotness
// counter. Returned value will be in 1 .. 127 inclusive.
static uint32_t BlockSizeToDownwardsStep(size_t blockBytecodeSize) {
MOZ_RELEASE_ASSERT(blockBytecodeSize <= size_t(MaxFunctionBytes));
const uint32_t BYTECODES_PER_STEP = 20; // tunable parameter
size_t step = blockBytecodeSize / BYTECODES_PER_STEP;
step = std::max<uint32_t>(step, 1);
step = std::min<uint32_t>(step, 127);
return uint32_t(step);
}
//////////////////////////////////////////////////////////////////////////////
//
// Function entry and exit
bool BaseCompiler::beginFunction() {
AutoCreatedBy acb(masm, "(wasm)BaseCompiler::beginFunction");
JitSpew(JitSpew_Codegen, "# ========================================");
JitSpew(JitSpew_Codegen, "# Emitting wasm baseline code");
JitSpew(JitSpew_Codegen,
"# beginFunction: start of function prologue for index %d",
(int)func_.index);
// Make a start on the stackmap for this function. Inspect the args so
// as to determine which of them are both in-memory and pointer-typed, and
// add entries to machineStackTracker as appropriate.
ArgTypeVector args(funcType());
size_t inboundStackArgBytes = StackArgAreaSizeUnaligned(args, ABIKind::Wasm);
MOZ_ASSERT(inboundStackArgBytes % sizeof(void*) == 0);
stackMapGenerator_.numStackArgBytes = inboundStackArgBytes;
MOZ_ASSERT(stackMapGenerator_.machineStackTracker.length() == 0);
if (!stackMapGenerator_.machineStackTracker.pushNonGCPointers(
stackMapGenerator_.numStackArgBytes / sizeof(void*))) {
return false;
}
// Identify GC-managed pointers passed on the stack.
for (ABIArgIter i(args, ABIKind::Wasm); !i.done(); i++) {
ABIArg argLoc = *i;
if (argLoc.kind() == ABIArg::Stack &&
args[i.index()] == MIRType::WasmAnyRef) {
uint32_t offset = argLoc.offsetFromArgBase();
MOZ_ASSERT(offset < inboundStackArgBytes);
MOZ_ASSERT(offset % sizeof(void*) == 0);
stackMapGenerator_.machineStackTracker.setGCPointer(offset /
sizeof(void*));
}
}
perfSpewer_.markStartOffset(masm.currentOffset());
perfSpewer_.recordOffset(masm, "Prologue");
GenerateFunctionPrologue(
masm, CallIndirectId::forFunc(codeMeta_, func_.index),
compilerEnv_.mode() != CompileMode::Once ? Some(func_.index) : Nothing(),
&offsets_);
// GenerateFunctionPrologue pushes exactly one wasm::Frame's worth of
// stuff, and none of the values are GC pointers. Hence:
if (!stackMapGenerator_.machineStackTracker.pushNonGCPointers(
sizeof(Frame) / sizeof(void*))) {
return false;
}
// Initialize DebugFrame fields before the stack overflow trap so that
// we have the invariant that all observable Frames in a debugEnabled
// Module have valid DebugFrames.
if (compilerEnv_.debugEnabled()) {
#ifdef JS_CODEGEN_ARM64
static_assert(DebugFrame::offsetOfFrame() % WasmStackAlignment == 0,
"aligned");
#endif
masm.reserveStack(DebugFrame::offsetOfFrame());
if (!stackMapGenerator_.machineStackTracker.pushNonGCPointers(
DebugFrame::offsetOfFrame() / sizeof(void*))) {
return false;
}
masm.store32(Imm32(func_.index), Address(masm.getStackPointer(),
DebugFrame::offsetOfFuncIndex()));
masm.store32(Imm32(0),
Address(masm.getStackPointer(), DebugFrame::offsetOfFlags()));
// No need to initialize cachedReturnJSValue_ or any ref-typed spilled
// register results, as they are traced if and only if a corresponding
// flag (hasCachedReturnJSValue or hasSpilledRefRegisterResult) is set.
}
// Generate a stack-overflow check and its associated stackmap.
fr.checkStack(ABINonArgReg0,
TrapSiteDesc(BytecodeOffset(func_.lineOrBytecode)));
ExitStubMapVector extras;
if (!stackMapGenerator_.generateStackmapEntriesForTrapExit(args, &extras)) {
return false;
}
if (!createStackMap("stack check", extras, masm.currentOffset(),
HasDebugFrameWithLiveRefs::No)) {
return false;
}
size_t reservedBytes = fr.fixedAllocSize() - masm.framePushed();
MOZ_ASSERT(0 == (reservedBytes % sizeof(void*)));
masm.reserveStack(reservedBytes);
fr.onFixedStackAllocated();
if (!stackMapGenerator_.machineStackTracker.pushNonGCPointers(
reservedBytes / sizeof(void*))) {
return false;
}
// Locals are stack allocated. Mark ref-typed ones in the stackmap
// accordingly.
for (const Local& l : localInfo_) {
// Locals that are stack arguments were already added to the stackmap
// before pushing the frame.
if (l.type == MIRType::WasmAnyRef && !l.isStackArgument()) {
uint32_t offs = fr.localOffsetFromSp(l);
MOZ_ASSERT(0 == (offs % sizeof(void*)));
stackMapGenerator_.machineStackTracker.setGCPointer(offs / sizeof(void*));
}
}
// Copy arguments from registers to stack.
for (ABIArgIter i(args, ABIKind::Wasm); !i.done(); i++) {
if (args.isSyntheticStackResultPointerArg(i.index())) {
// If there are stack results and the pointer to stack results
// was passed in a register, store it to the stack.
if (i->argInRegister()) {
fr.storeIncomingStackResultAreaPtr(RegPtr(i->gpr()));
}
// If we're in a debug frame, copy the stack result pointer arg
// to a well-known place.
if (compilerEnv_.debugEnabled()) {
Register target = ABINonArgReturnReg0;
fr.loadIncomingStackResultAreaPtr(RegPtr(target));
size_t debugFrameOffset =
masm.framePushed() - DebugFrame::offsetOfFrame();
size_t debugStackResultsPointerOffset =
debugFrameOffset + DebugFrame::offsetOfStackResultsPointer();
masm.storePtr(target, Address(masm.getStackPointer(),
debugStackResultsPointerOffset));
}
continue;
}
if (!i->argInRegister()) {
continue;
}
Local& l = localInfo_[args.naturalIndex(i.index())];
switch (i.mirType()) {
case MIRType::Int32:
fr.storeLocalI32(RegI32(i->gpr()), l);
break;
case MIRType::Int64:
fr.storeLocalI64(RegI64(i->gpr64()), l);
break;
case MIRType::WasmAnyRef: {
mozilla::DebugOnly<uint32_t> offs = fr.localOffsetFromSp(l);
MOZ_ASSERT(0 == (offs % sizeof(void*)));
fr.storeLocalRef(RegRef(i->gpr()), l);
// We should have just visited this local in the preceding loop.
MOZ_ASSERT(stackMapGenerator_.machineStackTracker.isGCPointer(
offs / sizeof(void*)));
break;
}
case MIRType::Double:
fr.storeLocalF64(RegF64(i->fpu()), l);
break;
case MIRType::Float32:
fr.storeLocalF32(RegF32(i->fpu()), l);
break;
#ifdef ENABLE_WASM_SIMD
case MIRType::Simd128:
fr.storeLocalV128(RegV128(i->fpu()), l);
break;
#endif
default:
MOZ_CRASH("Function argument type");
}
}
fr.zeroLocals(&ra);
fr.storeInstancePtr(InstanceReg);
if (compilerEnv_.debugEnabled()) {
insertBreakablePoint(CallSiteKind::EnterFrame);
if (!createStackMap("debug: enter-frame breakpoint")) {
return false;
}
}
JitSpew(JitSpew_Codegen,
"# beginFunction: enter body with masm.framePushed = %u",
masm.framePushed());
MOZ_ASSERT(stackMapGenerator_.framePushedAtEntryToBody.isNothing());
stackMapGenerator_.framePushedAtEntryToBody.emplace(masm.framePushed());
if (compilerEnv_.mode() == CompileMode::LazyTiering) {
size_t funcBytecodeSize = func_.end - func_.begin;
uint32_t step = BlockSizeToDownwardsStep(funcBytecodeSize);
// Create a patchable hotness check and patch it immediately (only because
// there's no way to directly create a non-patchable check directly).
Maybe<CodeOffset> ctrDecOffset = addHotnessCheck();
if (ctrDecOffset.isNothing()) {
return false;
}
patchHotnessCheck(ctrDecOffset.value(), step);
}
return true;
}
bool BaseCompiler::endFunction() {
AutoCreatedBy acb(masm, "(wasm)BaseCompiler::endFunction");
JitSpew(JitSpew_Codegen, "# endFunction: start of function epilogue");
// Always branch to returnLabel_.
masm.breakpoint();
// Patch the add in the prologue so that it checks against the correct
// frame size. Flush the constant pool in case it needs to be patched.
masm.flush();
// Precondition for patching.
if (masm.oom()) {
return false;
}
fr.patchCheckStack();
// We could skip generating the epilogue for functions which never return,
// but that would mess up invariants that all functions have a return address
// offset in CodeRange. It's also a rare thing, so not worth optimizing for.
deadCode_ = !returnLabel_.used();
masm.bind(&returnLabel_);
ResultType resultType(ResultType::Vector(funcType().results()));
popStackReturnValues(resultType);
if (compilerEnv_.debugEnabled() && !deadCode_) {
// Store and reload the return value from DebugFrame::return so that
// it can be clobbered, and/or modified by the debug trap.
saveRegisterReturnValues(resultType);
insertBreakablePoint(CallSiteKind::Breakpoint);
if (!createStackMap("debug: return-point breakpoint",
HasDebugFrameWithLiveRefs::Maybe)) {
return false;
}
insertBreakablePoint(CallSiteKind::LeaveFrame);
if (!createStackMap("debug: leave-frame breakpoint",
HasDebugFrameWithLiveRefs::Maybe)) {
return false;
}
restoreRegisterReturnValues(resultType);
}
#ifndef RABALDR_PIN_INSTANCE
// To satisy instance extent invariant we need to reload InstanceReg because
// baseline can clobber it.
fr.loadInstancePtr(InstanceReg);
#endif
perfSpewer_.recordOffset(masm, "Epilogue");
GenerateFunctionEpilogue(masm, fr.fixedAllocSize(), &offsets_);
#if defined(JS_ION_PERF)
// FIXME - profiling code missing. No bug for this.
// Note the end of the inline code and start of the OOL code.
// gen->perfSpewer().noteEndInlineCode(masm);
#endif
JitSpew(JitSpew_Codegen, "# endFunction: end of function epilogue");
JitSpew(JitSpew_Codegen, "# endFunction: start of OOL code");
perfSpewer_.recordOffset(masm, "OOLCode");
if (!generateOutOfLineCode()) {
return false;
}
JitSpew(JitSpew_Codegen, "# endFunction: end of OOL code");
if (compilerEnv_.debugEnabled()) {
JitSpew(JitSpew_Codegen, "# endFunction: start of per-function debug stub");
insertPerFunctionDebugStub();
JitSpew(JitSpew_Codegen, "# endFunction: end of per-function debug stub");
}
offsets_.end = masm.currentOffset();
if (!fr.checkStackHeight()) {
return decoder_.fail(decoder_.beginOffset(), "stack frame is too large");
}
JitSpew(JitSpew_Codegen, "# endFunction: end of OOL code for index %d",
(int)func_.index);
return !masm.oom();
}
//////////////////////////////////////////////////////////////////////////////
//
// Debugger API.
// [SMDOC] Wasm debug traps -- code details
//
// There are four pieces of code involved.
//
// (1) The "breakable point". This is placed at every location where we might
// want to transfer control to the debugger, most commonly before every
// bytecode. It must be as short and fast as possible. It checks
// Instance::debugStub_, which is either null or a pointer to (3). If
// non-null, a call to (2) is performed; when null, nothing happens.
//
// (2) The "per function debug stub". There is one per function. It consults
// a bit-vector attached to the Instance, to see whether breakpoints for
// the current function are enabled. If not, it returns (to (1), hence
// having no effect). Otherwise, it jumps (not calls) onwards to (3).
//
// (3) The "debug stub" -- not to be confused with the "per function debug
// stub". There is one per module. This saves all the registers and
// calls onwards to (4), which is in C++ land. When that call returns,
// (3) itself returns, which transfers control directly back to (after)
// (1).
//
// (4) In C++ land -- WasmHandleDebugTrap, corresponding to
// SymbolicAddress::HandleDebugTrap. This contains the detailed logic
// needed to handle the breakpoint.
void BaseCompiler::insertBreakablePoint(CallSiteKind kind) {
MOZ_ASSERT(!deadCode_);
#ifndef RABALDR_PIN_INSTANCE
fr.loadInstancePtr(InstanceReg);
#endif
// The breakpoint code must call the breakpoint handler installed on the
// instance if it is not null. There is one breakable point before
// every bytecode, and one at the beginning and at the end of the function.
//
// There are many constraints:
//
// - Code should be read-only; we do not want to patch
// - The breakpoint code should be as dense as possible, given the volume of
// breakable points
// - The handler-is-null case should be as fast as we can make it
//
// The scratch register is available here.
//
// An unconditional callout would be densest but is too slow. The best
// balance results from an inline test for null with a conditional call. The
// best code sequence is platform-dependent.
//
// The conditional call goes to a stub attached to the function that performs
// further filtering before calling the breakpoint handler.
#if defined(JS_CODEGEN_X64)
// REX 83 MODRM OFFS IB
static_assert(Instance::offsetOfDebugStub() < 128);
masm.cmpq(Imm32(0),
Operand(Address(InstanceReg, Instance::offsetOfDebugStub())));
// 74 OFFS
Label L;
L.bind(masm.currentOffset() + 7);
masm.j(Assembler::Zero, &L);
// E8 OFFS OFFS OFFS OFFS
masm.call(&perFunctionDebugStub_);
masm.append(CallSiteDesc(iter_.lastOpcodeOffset(), kind),
CodeOffset(masm.currentOffset()));
// Branch destination
MOZ_ASSERT_IF(!masm.oom(), masm.currentOffset() == uint32_t(L.offset()));
#elif defined(JS_CODEGEN_X86)
// 83 MODRM OFFS IB
static_assert(Instance::offsetOfDebugStub() < 128);
masm.cmpl(Imm32(0),
Operand(Address(InstanceReg, Instance::offsetOfDebugStub())));
// 74 OFFS
Label L;
L.bind(masm.currentOffset() + 7);
masm.j(Assembler::Zero, &L);
// E8 OFFS OFFS OFFS OFFS
masm.call(&perFunctionDebugStub_);
masm.append(CallSiteDesc(iter_.lastOpcodeOffset(), kind),
CodeOffset(masm.currentOffset()));
// Branch destination
MOZ_ASSERT_IF(!masm.oom(), masm.currentOffset() == uint32_t(L.offset()));
#elif defined(JS_CODEGEN_ARM64)
ScratchPtr scratch(*this);
ARMRegister tmp(scratch, 64);
Label L;
masm.Ldr(tmp,
MemOperand(Address(InstanceReg, Instance::offsetOfDebugStub())));
masm.Cbz(tmp, &L);
masm.Bl(&perFunctionDebugStub_);
masm.append(CallSiteDesc(iter_.lastOpcodeOffset(), kind),
CodeOffset(masm.currentOffset()));
masm.bind(&L);
#elif defined(JS_CODEGEN_ARM)
ScratchPtr scratch(*this);
masm.loadPtr(Address(InstanceReg, Instance::offsetOfDebugStub()), scratch);
masm.ma_orr(scratch, scratch, SetCC);
masm.ma_bl(&perFunctionDebugStub_, Assembler::NonZero);
masm.append(CallSiteDesc(iter_.lastOpcodeOffset(), kind),
CodeOffset(masm.currentOffset()));
#elif defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_MIPS64) || \
defined(JS_CODEGEN_RISCV64)
ScratchPtr scratch(*this);
Label L;
masm.loadPtr(Address(InstanceReg, Instance::offsetOfDebugStub()), scratch);
masm.branchPtr(Assembler::Equal, scratch, ImmWord(0), &L);
masm.call(&perFunctionDebugStub_);
masm.append(CallSiteDesc(iter_.lastOpcodeOffset(), kind),
CodeOffset(masm.currentOffset()));
masm.bind(&L);
#else
MOZ_CRASH("BaseCompiler platform hook: insertBreakablePoint");
#endif
}
void BaseCompiler::insertPerFunctionDebugStub() {
// The per-function debug stub performs out-of-line filtering before jumping
// to the per-module debug stub if necessary. The per-module debug stub
// returns directly to the breakable point.
//
// NOTE, the link register is live here on platforms that have LR.
//
// The scratch register is available here (as it was at the call site).
//
// It's useful for the per-function debug stub to be compact, as every
// function gets one.
Label L;
masm.bind(&perFunctionDebugStub_);
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
{
ScratchPtr scratch(*this);
// Get the per-instance table of filtering bits.
masm.loadPtr(Address(InstanceReg, Instance::offsetOfDebugFilter()),
scratch);
// Check the filter bit. There is one bit per function in the module.
// Table elements are 32-bit because the masm makes that convenient.
masm.branchTest32(Assembler::NonZero, Address(scratch, func_.index / 32),
Imm32(1 << (func_.index % 32)), &L);
// Fast path: return to the execution.
masm.ret();
}
#elif defined(JS_CODEGEN_ARM64)
{
ScratchPtr scratch(*this);
// Logic as above, except abiret to jump to the LR directly
masm.loadPtr(Address(InstanceReg, Instance::offsetOfDebugFilter()),
scratch);
masm.branchTest32(Assembler::NonZero, Address(scratch, func_.index / 32),
Imm32(1 << (func_.index % 32)), &L);
masm.abiret();
}
#elif defined(JS_CODEGEN_ARM)
{
// We must be careful not to use the SecondScratchRegister, which usually
// is LR, as LR is live here. This means avoiding masm abstractions such
// as branchTest32.
static_assert(ScratchRegister != lr);
static_assert(Instance::offsetOfDebugFilter() < 0x1000);
ScratchRegisterScope tmp1(masm);
ScratchI32 tmp2(*this);
masm.ma_ldr(
DTRAddr(InstanceReg, DtrOffImm(Instance::offsetOfDebugFilter())), tmp1);
masm.ma_mov(Imm32(func_.index / 32), tmp2);
masm.ma_ldr(DTRAddr(tmp1, DtrRegImmShift(tmp2, LSL, 0)), tmp2);
masm.ma_tst(tmp2, Imm32(1 << func_.index % 32), tmp1, Assembler::Always);
masm.ma_bx(lr, Assembler::Zero);
}
#elif defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_MIPS64) || \
defined(JS_CODEGEN_RISCV64)
{
ScratchPtr scratch(*this);
// Logic same as ARM64.
masm.loadPtr(Address(InstanceReg, Instance::offsetOfDebugFilter()),
scratch);
masm.branchTest32(Assembler::NonZero, Address(scratch, func_.index / 32),
Imm32(1 << (func_.index % 32)), &L);
masm.abiret();
}
#else
MOZ_CRASH("BaseCompiler platform hook: endFunction");
#endif
// Jump to the per-module debug stub, which calls onwards to C++ land.
masm.bind(&L);
masm.jump(Address(InstanceReg, Instance::offsetOfDebugStub()));
}
void BaseCompiler::saveRegisterReturnValues(const ResultType& resultType) {
MOZ_ASSERT(compilerEnv_.debugEnabled());
size_t debugFrameOffset = masm.framePushed() - DebugFrame::offsetOfFrame();
size_t registerResultIdx = 0;
for (ABIResultIter i(resultType); !i.done(); i.next()) {
const ABIResult result = i.cur();
if (!result.inRegister()) {
#ifdef DEBUG
for (i.next(); !i.done(); i.next()) {
MOZ_ASSERT(!i.cur().inRegister());
}
#endif
break;
}
size_t resultOffset = DebugFrame::offsetOfRegisterResult(registerResultIdx);
Address dest(masm.getStackPointer(), debugFrameOffset + resultOffset);
switch (result.type().kind()) {
case ValType::I32:
masm.store32(RegI32(result.gpr()), dest);
break;
case ValType::I64:
masm.store64(RegI64(result.gpr64()), dest);
break;
case ValType::F64:
masm.storeDouble(RegF64(result.fpr()), dest);
break;
case ValType::F32:
masm.storeFloat32(RegF32(result.fpr()), dest);
break;
case ValType::Ref: {
uint32_t flag =
DebugFrame::hasSpilledRegisterRefResultBitMask(registerResultIdx);
// Tell Instance::traceFrame that we have a pointer to trace.
masm.or32(Imm32(flag),
Address(masm.getStackPointer(),
debugFrameOffset + DebugFrame::offsetOfFlags()));
masm.storePtr(RegRef(result.gpr()), dest);
break;
}
case ValType::V128:
#ifdef ENABLE_WASM_SIMD
masm.storeUnalignedSimd128(RegV128(result.fpr()), dest);
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
registerResultIdx++;
}
}
void BaseCompiler::restoreRegisterReturnValues(const ResultType& resultType) {
MOZ_ASSERT(compilerEnv_.debugEnabled());
size_t debugFrameOffset = masm.framePushed() - DebugFrame::offsetOfFrame();
size_t registerResultIdx = 0;
for (ABIResultIter i(resultType); !i.done(); i.next()) {
const ABIResult result = i.cur();
if (!result.inRegister()) {
#ifdef DEBUG
for (i.next(); !i.done(); i.next()) {
MOZ_ASSERT(!i.cur().inRegister());
}
#endif
break;
}
size_t resultOffset =
DebugFrame::offsetOfRegisterResult(registerResultIdx++);
Address src(masm.getStackPointer(), debugFrameOffset + resultOffset);
switch (result.type().kind()) {
case ValType::I32:
masm.load32(src, RegI32(result.gpr()));
break;
case ValType::I64:
masm.load64(src, RegI64(result.gpr64()));
break;
case ValType::F64:
masm.loadDouble(src, RegF64(result.fpr()));
break;
case ValType::F32:
masm.loadFloat32(src, RegF32(result.fpr()));
break;
case ValType::Ref:
masm.loadPtr(src, RegRef(result.gpr()));
break;
case ValType::V128:
#ifdef ENABLE_WASM_SIMD
masm.loadUnalignedSimd128(src, RegV128(result.fpr()));
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Support for lazy tiering
// The key thing here is, we generate a short piece of code which, most of the
// time, has no effect, but just occasionally wants to call out to C++ land.
// That's a similar requirement to the Debugger API support (see above) and so
// we have a similar, but simpler, solution. Specifically, we use a single
// stub routine for the whole module, whereas for debugging, there are
// per-function stub routines as well as a whole-module stub routine involved.
class OutOfLineRequestTierUp : public OutOfLineCode {
Register instance_; // points at the instance at entry; must remain unchanged
Maybe<RegI32> scratch_; // only provided on arm32
size_t lastOpcodeOffset_; // a bytecode offset
public:
OutOfLineRequestTierUp(Register instance, Maybe<RegI32> scratch,
size_t lastOpcodeOffset)
: instance_(instance),
scratch_(scratch),
lastOpcodeOffset_(lastOpcodeOffset) {}
virtual void generate(MacroAssembler* masm) override {
// Generate:
//
// [optionally, if `instance_` != InstanceReg: swap(instance_, InstanceReg)]
// call * $offsetOfRequestTierUpStub(InstanceReg)
// [optionally, if `instance_` != InstanceReg: swap(instance_, InstanceReg)]
// goto rejoin
//
// This is the unlikely path, where we call the (per-module)
// request-tier-up stub. The stub wants the instance pointer to be in the
// official InstanceReg at this point, but InstanceReg itself might hold
// arbitrary other live data. Hence, if necessary, swap `instance_` and
// InstanceReg before the call and swap them back after it.
#ifndef RABALDR_PIN_INSTANCE
if (Register(instance_) != InstanceReg) {
# ifdef JS_CODEGEN_X86
// On x86_32 this is easy.
masm->xchgl(instance_, InstanceReg);
# elif JS_CODEGEN_ARM
masm->mov(instance_,
scratch_.value()); // note, destination is second arg
masm->mov(InstanceReg, instance_);
masm->mov(scratch_.value(), InstanceReg);
# else
MOZ_CRASH("BaseCompiler::OutOfLineRequestTierUp #1");
# endif
}
#endif
// Call the stub
masm->call(Address(InstanceReg, Instance::offsetOfRequestTierUpStub()));
masm->append(CallSiteDesc(lastOpcodeOffset_, CallSiteKind::RequestTierUp),
CodeOffset(masm->currentOffset()));
// And swap again, if we swapped above.
#ifndef RABALDR_PIN_INSTANCE
if (Register(instance_) != InstanceReg) {
# ifdef JS_CODEGEN_X86
masm->xchgl(instance_, InstanceReg);
# elif JS_CODEGEN_ARM
masm->mov(instance_, scratch_.value());
masm->mov(InstanceReg, instance_);
masm->mov(scratch_.value(), InstanceReg);
# else
MOZ_CRASH("BaseCompiler::OutOfLineRequestTierUp #2");
# endif
}
#endif
masm->jump(rejoin());
}
};
Maybe<CodeOffset> BaseCompiler::addHotnessCheck() {
// Here's an example of what we'll create. The path that almost always
// happens, where the counter doesn't go negative, has just one branch.
//
// subl $to_be_filled_in_later, 0x170(%r14)
// js oolCode // almost never taken
// rejoin:
// ----------------
// oolCode: // we get here when the counter is negative, viz, almost never
// call *0x160(%r14) // RequestTierUpStub
// jmp rejoin
//
// Note that the counter is updated regardless of whether or not it has gone
// negative. That means that, at entry to RequestTierUpStub, we know the
// counter must be negative, and not merely zero.
//
// Non-Intel targets will have to generate a load / subtract-and-set-flags /
// store / jcond sequence.
//
// To ensure the shortest possible encoding, `to_be_filled_in_later` must be
// a value in the range 1 .. 127 inclusive. This is good enough for
// hotness-counting purposes.
AutoCreatedBy acb(masm, "BC::addHotnessCheck");
#ifdef RABALDR_PIN_INSTANCE
Register instance(InstanceReg);
#else
// This seems to assume that any non-RABALDR_PIN_INSTANCE target is 32-bit
ScratchI32 instance(*this);
fr.loadInstancePtr(instance);
#endif
Address addressOfCounter = Address(
instance, wasm::Instance::offsetInData(
codeMeta_.offsetOfFuncDefInstanceData(func_.index)));
#if JS_CODEGEN_ARM
Maybe<RegI32> scratch = Some(needI32());
#else
Maybe<RegI32> scratch = Nothing();
#endif
OutOfLineCode* ool = addOutOfLineCode(new (alloc_) OutOfLineRequestTierUp(
instance, scratch, iter_.lastOpcodeOffset()));
if (!ool) {
return Nothing();
}
// Because of the Intel arch instruction formats, `patchPoint` points to the
// byte immediately following the last byte of the instruction to patch.
CodeOffset patchPoint = masm.sub32FromMemAndBranchIfNegativeWithPatch(
addressOfCounter, ool->entry());
masm.bind(ool->rejoin());
if (scratch.isSome()) {
freeI32(scratch.value());
}
// `patchPoint` might be invalid if the assembler OOMd at some point.
return masm.oom() ? Nothing() : Some(patchPoint);
}
void BaseCompiler::patchHotnessCheck(CodeOffset offset, uint32_t step) {
// Zero makes the hotness check pointless. Above 127 is not representable in
// the short-form Intel encoding.
MOZ_RELEASE_ASSERT(step > 0 && step <= 127);
MOZ_ASSERT(!masm.oom());
masm.patchSub32FromMemAndBranchIfNegative(offset, Imm32(step));
}
//////////////////////////////////////////////////////////////////////////////
//
// Results and block parameters
void BaseCompiler::popStackReturnValues(const ResultType& resultType) {
uint32_t bytes = ABIResultIter::MeasureStackBytes(resultType);
if (bytes == 0) {
return;
}
Register target = ABINonArgReturnReg0;
Register temp = ABINonArgReturnReg1;
fr.loadIncomingStackResultAreaPtr(RegPtr(target));
fr.popStackResultsToMemory(target, bytes, temp);
}
// TODO / OPTIMIZE (Bug 1316818): At the moment we use the Wasm
// inter-procedure ABI for block returns, which allocates ReturnReg as the
// single block result register. It is possible other choices would lead to
// better register allocation, as ReturnReg is often first in the register set
// and will be heavily wanted by the register allocator that uses takeFirst().
//
// Obvious options:
// - pick a register at the back of the register set
// - pick a random register per block (different blocks have
// different join regs)
void BaseCompiler::popRegisterResults(ABIResultIter& iter) {
// Pop register results. Note that in the single-value case, popping to a
// register may cause a sync(); for multi-value we sync'd already.
for (; !iter.done(); iter.next()) {
const ABIResult& result = iter.cur();
if (!result.inRegister()) {
// TODO / OPTIMIZE: We sync here to avoid solving the general parallel
// move problem in popStackResults. However we could avoid syncing the
// values that are going to registers anyway, if they are already in
// registers.
sync();
break;
}
switch (result.type().kind()) {
case ValType::I32:
popI32(RegI32(result.gpr()));
break;
case ValType::I64:
popI64(RegI64(result.gpr64()));
break;
case ValType::F32:
popF32(RegF32(result.fpr()));
break;
case ValType::F64:
popF64(RegF64(result.fpr()));
break;
case ValType::Ref:
popRef(RegRef(result.gpr()));
break;
case ValType::V128:
#ifdef ENABLE_WASM_SIMD
popV128(RegV128(result.fpr()));
#else
MOZ_CRASH("No SIMD support");
#endif
}
}
}
void BaseCompiler::popStackResults(ABIResultIter& iter, StackHeight stackBase) {
MOZ_ASSERT(!iter.done());
// The iterator should be advanced beyond register results, and register
// results should be popped already from the value stack.
uint32_t alreadyPopped = iter.index();
// At this point, only stack arguments are remaining. Iterate through them
// to measure how much stack space they will take up.
for (; !iter.done(); iter.next()) {
MOZ_ASSERT(iter.cur().onStack());
}
// Calculate the space needed to store stack results, in bytes.
uint32_t stackResultBytes = iter.stackBytesConsumedSoFar();
MOZ_ASSERT(stackResultBytes);
// Compute the stack height including the stack results. Note that it's
// possible that this call expands the stack, for example if some of the
// results are supplied by constants and so are not already on the machine
// stack.
uint32_t endHeight = fr.prepareStackResultArea(stackBase, stackResultBytes);
// Find a free GPR to use when shuffling stack values. If none is
// available, push ReturnReg and restore it after we're done.
bool saved = false;
RegPtr temp = ra.needTempPtr(RegPtr(ReturnReg), &saved);
// The sequence of Stk values is in the same order on the machine stack as
// the result locations, but there is a complication: constant values are
// not actually pushed on the machine stack. (At this point registers and
// locals have been spilled already.) So, moving the Stk values into place
// isn't simply a shuffle-down or shuffle-up operation. There is a part of
// the Stk sequence that shuffles toward the FP, a part that's already in
// place, and a part that shuffles toward the SP. After shuffling, we have
// to materialize the constants.
// Shuffle mem values toward the frame pointer, copying deepest values
// first. Stop when we run out of results, get to a register result, or
// find a Stk value that is closer to the FP than the result.
for (iter.switchToPrev(); !iter.done(); iter.prev()) {
const ABIResult& result = iter.cur();
if (!result.onStack()) {
break;
}
MOZ_ASSERT(result.stackOffset() < stackResultBytes);
uint32_t destHeight = endHeight - result.stackOffset();
uint32_t stkBase = stk_.length() - (iter.count() - alreadyPopped);
Stk& v = stk_[stkBase + iter.index()];
if (v.isMem()) {
uint32_t srcHeight = v.offs();
if (srcHeight <= destHeight) {
break;
}
fr.shuffleStackResultsTowardFP(srcHeight, destHeight, result.size(),
temp);
}
}
// Reset iterator and skip register results.
for (iter.reset(); !iter.done(); iter.next()) {
if (iter.cur().onStack()) {
break;
}
}
// Revisit top stack values, shuffling mem values toward the stack pointer,
// copying shallowest values first.
for (; !iter.done(); iter.next()) {
const ABIResult& result = iter.cur();
MOZ_ASSERT(result.onStack());
MOZ_ASSERT(result.stackOffset() < stackResultBytes);
uint32_t destHeight = endHeight - result.stackOffset();
Stk& v = stk_[stk_.length() - (iter.index() - alreadyPopped) - 1];
if (v.isMem()) {
uint32_t srcHeight = v.offs();
if (srcHeight >= destHeight) {
break;
}
fr.shuffleStackResultsTowardSP(srcHeight, destHeight, result.size(),
temp);
}
}
// Reset iterator and skip register results, which are already popped off
// the value stack.
for (iter.reset(); !iter.done(); iter.next()) {
if (iter.cur().onStack()) {
break;
}
}
// Materialize constants and pop the remaining items from the value stack.
for (; !iter.done(); iter.next()) {
const ABIResult& result = iter.cur();
uint32_t resultHeight = endHeight - result.stackOffset();
Stk& v = stk_.back();
switch (v.kind()) {
case Stk::ConstI32:
#if defined(JS_CODEGEN_MIPS64) || defined(JS_CODEGEN_LOONG64) || \
defined(JS_CODEGEN_RISCV64)
fr.storeImmediatePtrToStack(v.i32val_, resultHeight, temp);
#else
fr.storeImmediatePtrToStack(uint32_t(v.i32val_), resultHeight, temp);
#endif
break;
case Stk::ConstF32:
fr.storeImmediateF32ToStack(v.f32val_, resultHeight, temp);
break;
case Stk::ConstI64:
fr.storeImmediateI64ToStack(v.i64val_, resultHeight, temp);
break;
case Stk::ConstF64:
fr.storeImmediateF64ToStack(v.f64val_, resultHeight, temp);
break;
#ifdef ENABLE_WASM_SIMD
case Stk::ConstV128:
fr.storeImmediateV128ToStack(v.v128val_, resultHeight, temp);
break;
#endif
case Stk::ConstRef:
fr.storeImmediatePtrToStack(v.refval_, resultHeight, temp);
break;
case Stk::MemRef:
// Update bookkeeping as we pop the Stk entry.
stackMapGenerator_.memRefsOnStk--;
break;
default:
MOZ_ASSERT(v.isMem());
break;
}
stk_.popBack();
}
ra.freeTempPtr(temp, saved);
// This will pop the stack if needed.
fr.finishStackResultArea(stackBase, stackResultBytes);
}
void BaseCompiler::popBlockResults(ResultType type, StackHeight stackBase,
ContinuationKind kind) {
if (!type.empty()) {
ABIResultIter iter(type);
popRegisterResults(iter);
if (!iter.done()) {
popStackResults(iter, stackBase);
// Because popStackResults might clobber the stack, it leaves the stack
// pointer already in the right place for the continuation, whether the
// continuation is a jump or fallthrough.
return;
}
}
// We get here if there are no stack results. For a fallthrough, the stack
// is already at the right height. For a jump, we may need to pop the stack
// pointer if the continuation's stack height is lower than the current
// stack height.
if (kind == ContinuationKind::Jump) {
fr.popStackBeforeBranch(stackBase, type);
}
}
// This function is similar to popBlockResults, but additionally handles the
// implicit exception pointer that is pushed to the value stack on entry to
// a catch handler by dropping it appropriately.
void BaseCompiler::popCatchResults(ResultType type, StackHeight stackBase) {
if (!type.empty()) {
ABIResultIter iter(type);
popRegisterResults(iter);
if (!iter.done()) {
popStackResults(iter, stackBase);
// Since popStackResults clobbers the stack, we only need to free the
// exception off of the value stack.
popValueStackBy(1);
} else {
// If there are no stack results, we have to adjust the stack by
// dropping the exception reference that's now on the stack.
dropValue();
}
} else {
dropValue();
}
fr.popStackBeforeBranch(stackBase, type);
}
Stk BaseCompiler::captureStackResult(const ABIResult& result,
StackHeight resultsBase,
uint32_t stackResultBytes) {
MOZ_ASSERT(result.onStack());
uint32_t offs = fr.locateStackResult(result, resultsBase, stackResultBytes);
return Stk::StackResult(result.type(), offs);
}
// TODO: It may be fruitful to inline the fast path here, as it will be common.
bool BaseCompiler::pushResults(ResultType type, StackHeight resultsBase) {
if (type.empty()) {
return true;
}
if (type.length() > 1) {
// Reserve extra space on the stack for all the values we'll push.
// Multi-value push is not accounted for by the pre-sizing of the stack in
// the decoding loop.
//
// Also make sure we leave headroom for other pushes that will occur after
// pushing results, just to be safe.
if (!stk_.reserve(stk_.length() + type.length() + MaxPushesPerOpcode)) {
return false;
}
}
// We need to push the results in reverse order, so first iterate through
// all results to determine the locations of stack result types.
ABIResultIter iter(type);
while (!iter.done()) {
iter.next();
}
uint32_t stackResultBytes = iter.stackBytesConsumedSoFar();
for (iter.switchToPrev(); !iter.done(); iter.prev()) {
const ABIResult& result = iter.cur();
if (!result.onStack()) {
break;
}
Stk v = captureStackResult(result, resultsBase, stackResultBytes);
push(v);
if (v.kind() == Stk::MemRef) {
stackMapGenerator_.memRefsOnStk++;
}
}
for (; !iter.done(); iter.prev()) {
const ABIResult& result = iter.cur();
MOZ_ASSERT(result.inRegister());
switch (result.type().kind()) {
case ValType::I32:
pushI32(RegI32(result.gpr()));
break;
case ValType::I64:
pushI64(RegI64(result.gpr64()));
break;
case ValType::V128:
#ifdef ENABLE_WASM_SIMD
pushV128(RegV128(result.fpr()));
break;
#else
MOZ_CRASH("No SIMD support");
#endif
case ValType::F32:
pushF32(RegF32(result.fpr()));
break;
case ValType::F64:
pushF64(RegF64(result.fpr()));
break;
case ValType::Ref:
pushRef(RegRef(result.gpr()));
break;
}
}
return true;
}
bool BaseCompiler::pushBlockResults(ResultType type) {
return pushResults(type, controlItem().stackHeight);
}
// A combination of popBlockResults + pushBlockResults, used when entering a
// block with a control-flow join (loops) or split (if) to shuffle the
// fallthrough block parameters into the locations expected by the
// continuation.
bool BaseCompiler::topBlockParams(ResultType type) {
// This function should only be called when entering a block with a
// control-flow join at the entry, where there are no live temporaries in
// the current block.
StackHeight base = controlItem().stackHeight;
MOZ_ASSERT(fr.stackResultsBase(stackConsumed(type.length())) == base);
popBlockResults(type, base, ContinuationKind::Fallthrough);
return pushBlockResults(type);
}
// A combination of popBlockResults + pushBlockResults, used before branches
// where we don't know the target (br_if / br_table). If and when the branch
// is taken, the stack results will be shuffled down into place. For br_if
// that has fallthrough, the parameters for the untaken branch flow through to
// the continuation.
bool BaseCompiler::topBranchParams(ResultType type, StackHeight* height) {
if (type.empty()) {
*height = fr.stackHeight();
return true;
}
// There may be temporary values that need spilling; delay computation of
// the stack results base until after the popRegisterResults(), which spills
// if needed.
ABIResultIter iter(type);
popRegisterResults(iter);
StackHeight base = fr.stackResultsBase(stackConsumed(iter.remaining()));
if (!iter.done()) {
popStackResults(iter, base);
}
if (!pushResults(type, base)) {
return false;
}
*height = base;
return true;
}
// Conditional branches with fallthrough are preceded by a topBranchParams, so
// we know that there are no stack results that need to be materialized. In
// that case, we can just shuffle the whole block down before popping the
// stack.
void BaseCompiler::shuffleStackResultsBeforeBranch(StackHeight srcHeight,
StackHeight destHeight,
ResultType type) {
uint32_t stackResultBytes = 0;
if (ABIResultIter::HasStackResults(type)) {
MOZ_ASSERT(stk_.length() >= type.length());
ABIResultIter iter(type);
for (; !iter.done(); iter.next()) {
#ifdef DEBUG
const ABIResult& result = iter.cur();
const Stk& v = stk_[stk_.length() - iter.index() - 1];
MOZ_ASSERT(v.isMem() == result.onStack());
#endif
}
stackResultBytes = iter.stackBytesConsumedSoFar();
MOZ_ASSERT(stackResultBytes > 0);
if (srcHeight != destHeight) {
// Find a free GPR to use when shuffling stack values. If none
// is available, push ReturnReg and restore it after we're done.
bool saved = false;
RegPtr temp = ra.needTempPtr(RegPtr(ReturnReg), &saved);
fr.shuffleStackResultsTowardFP(srcHeight, destHeight, stackResultBytes,
temp);
ra.freeTempPtr(temp, saved);
}
}
fr.popStackBeforeBranch(destHeight, stackResultBytes);
}
bool BaseCompiler::insertDebugCollapseFrame() {
if (!compilerEnv_.debugEnabled() || deadCode_) {
return true;
}
insertBreakablePoint(CallSiteKind::CollapseFrame);
return createStackMap("debug: collapse-frame breakpoint",
HasDebugFrameWithLiveRefs::Maybe);
}
//////////////////////////////////////////////////////////////////////////////
//
// Function calls.
void BaseCompiler::beginCall(FunctionCall& call) {
// Use masm.framePushed() because the value we want here does not depend
// on the height of the frame's stack area, but the actual size of the
// allocated frame.
call.frameAlignAdjustment = ComputeByteAlignment(
masm.framePushed() + sizeof(Frame), JitStackAlignment);
}
void BaseCompiler::endCall(FunctionCall& call, size_t stackSpace) {
size_t adjustment = call.stackArgAreaSize + call.frameAlignAdjustment;
fr.freeArgAreaAndPopBytes(adjustment, stackSpace);
MOZ_ASSERT(stackMapGenerator_.framePushedExcludingOutboundCallArgs.isSome());
stackMapGenerator_.framePushedExcludingOutboundCallArgs.reset();
if (call.restoreState == RestoreState::All) {
fr.loadInstancePtr(InstanceReg);
masm.loadWasmPinnedRegsFromInstance(mozilla::Nothing());
masm.switchToWasmInstanceRealm(ABINonArgReturnReg0, ABINonArgReturnReg1);
} else if (call.restoreState == RestoreState::PinnedRegs) {
masm.loadWasmPinnedRegsFromInstance(mozilla::Nothing());
}
}
void BaseCompiler::startCallArgs(size_t stackArgAreaSizeUnaligned,
FunctionCall* call) {
size_t stackArgAreaSizeAligned =
AlignStackArgAreaSize(stackArgAreaSizeUnaligned);
MOZ_ASSERT(stackArgAreaSizeUnaligned <= stackArgAreaSizeAligned);
// Record the masm.framePushed() value at this point, before we push args
// for the call and any required alignment space. This defines the lower limit
// of the stackmap that will be created for this call.
MOZ_ASSERT(
stackMapGenerator_.framePushedExcludingOutboundCallArgs.isNothing());
stackMapGenerator_.framePushedExcludingOutboundCallArgs.emplace(
// However much we've pushed so far
masm.framePushed() +
// Extra space we'll push to get the frame aligned
call->frameAlignAdjustment);
call->stackArgAreaSize = stackArgAreaSizeAligned;
size_t adjustment = call->stackArgAreaSize + call->frameAlignAdjustment;
fr.allocArgArea(adjustment);
}
ABIArg BaseCompiler::reservePointerArgument(FunctionCall* call) {
return call->abi.next(MIRType::Pointer);
}
// TODO / OPTIMIZE (Bug 1316821): Note passArg is used only in one place.
// (Or it was, until Luke wandered through, but that can be fixed again.)
// I'm not saying we should manually inline it, but we could hoist the
// dispatch into the caller and have type-specific implementations of
// passArg: passArgI32(), etc. Then those might be inlined, at least in PGO
// builds.
//
// The bulk of the work here (60%) is in the next() call, though.
//
// Notably, since next() is so expensive, StackArgAreaSizeUnaligned()
// becomes expensive too.
//
// Somehow there could be a trick here where the sequence of argument types
// (read from the input stream) leads to a cached entry for
// StackArgAreaSizeUnaligned() and for how to pass arguments...
//
// But at least we could reduce the cost of StackArgAreaSizeUnaligned() by
// first reading the argument types into a (reusable) vector, then we have
// the outgoing size at low cost, and then we can pass args based on the
// info we read.
void BaseCompiler::passArg(ValType type, const Stk& arg, FunctionCall* call) {
switch (type.kind()) {
case ValType::I32: {
ABIArg argLoc = call->abi.next(MIRType::Int32);
if (argLoc.kind() == ABIArg::Stack) {
ScratchI32 scratch(*this);
loadI32(arg, scratch);
masm.store32(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
} else {
loadI32(arg, RegI32(argLoc.gpr()));
}
break;
}
case ValType::I64: {
ABIArg argLoc = call->abi.next(MIRType::Int64);
if (argLoc.kind() == ABIArg::Stack) {
ScratchI32 scratch(*this);
#ifdef JS_PUNBOX64
loadI64(arg, fromI32(scratch));
masm.storePtr(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
#else
loadI64Low(arg, scratch);
masm.store32(scratch, LowWord(Address(masm.getStackPointer(),
argLoc.offsetFromArgBase())));
loadI64High(arg, scratch);
masm.store32(scratch, HighWord(Address(masm.getStackPointer(),
argLoc.offsetFromArgBase())));
#endif
} else {
loadI64(arg, RegI64(argLoc.gpr64()));
}
break;
}
case ValType::V128: {
#ifdef ENABLE_WASM_SIMD
ABIArg argLoc = call->abi.next(MIRType::Simd128);
switch (argLoc.kind()) {
case ABIArg::Stack: {
ScratchV128 scratch(*this);
loadV128(arg, scratch);
masm.storeUnalignedSimd128(
(RegV128)scratch,
Address(masm.getStackPointer(), argLoc.offsetFromArgBase()));
break;
}
case ABIArg::GPR: {
MOZ_CRASH("Unexpected parameter passing discipline");
}
case ABIArg::FPU: {
loadV128(arg, RegV128(argLoc.fpu()));
break;
}
# if defined(JS_CODEGEN_REGISTER_PAIR)
case ABIArg::GPR_PAIR: {
MOZ_CRASH("Unexpected parameter passing discipline");
}
# endif
case ABIArg::Uninitialized:
MOZ_CRASH("Uninitialized ABIArg kind");
}
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
case ValType::F64: {
ABIArg argLoc = call->abi.next(MIRType::Double);
switch (argLoc.kind()) {
case ABIArg::Stack: {
ScratchF64 scratch(*this);
loadF64(arg, scratch);
masm.storeDouble(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
break;
}
#if defined(JS_CODEGEN_REGISTER_PAIR)
case ABIArg::GPR_PAIR: {
# if defined(JS_CODEGEN_ARM)
ScratchF64 scratch(*this);
loadF64(arg, scratch);
masm.ma_vxfer(scratch, argLoc.evenGpr(), argLoc.oddGpr());
break;
# else
MOZ_CRASH("BaseCompiler platform hook: passArg F64 pair");
# endif
}
#endif
case ABIArg::FPU: {
loadF64(arg, RegF64(argLoc.fpu()));
break;
}
case ABIArg::GPR: {
MOZ_CRASH("Unexpected parameter passing discipline");
}
case ABIArg::Uninitialized:
MOZ_CRASH("Uninitialized ABIArg kind");
}
break;
}
case ValType::F32: {
ABIArg argLoc = call->abi.next(MIRType::Float32);
switch (argLoc.kind()) {
case ABIArg::Stack: {
ScratchF32 scratch(*this);
loadF32(arg, scratch);
masm.storeFloat32(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
break;
}
case ABIArg::GPR: {
ScratchF32 scratch(*this);
loadF32(arg, scratch);
masm.moveFloat32ToGPR(scratch, argLoc.gpr());
break;
}
case ABIArg::FPU: {
loadF32(arg, RegF32(argLoc.fpu()));
break;
}
#if defined(JS_CODEGEN_REGISTER_PAIR)
case ABIArg::GPR_PAIR: {
MOZ_CRASH("Unexpected parameter passing discipline");
}
#endif
case ABIArg::Uninitialized:
MOZ_CRASH("Uninitialized ABIArg kind");
}
break;
}
case ValType::Ref: {
ABIArg argLoc = call->abi.next(MIRType::WasmAnyRef);
if (argLoc.kind() == ABIArg::Stack) {
ScratchRef scratch(*this);
loadRef(arg, scratch);
masm.storePtr(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
} else {
loadRef(arg, RegRef(argLoc.gpr()));
}
break;
}
}
}
template <typename T>
bool BaseCompiler::emitCallArgs(const ValTypeVector& argTypes, T results,
FunctionCall* baselineCall,
CalleeOnStack calleeOnStack) {
MOZ_ASSERT(!deadCode_);
ArgTypeVector args(argTypes, results.stackResults());
uint32_t naturalArgCount = argTypes.length();
uint32_t abiArgCount = args.lengthWithStackResults();
startCallArgs(StackArgAreaSizeUnaligned(args, baselineCall->abiKind),
baselineCall);
// Args are deeper on the stack than the stack result area, if any.
size_t argsDepth = results.onStackCount();
// They're deeper than the callee too, for callIndirect.
if (calleeOnStack == CalleeOnStack::True) {
argsDepth++;
}
for (size_t i = 0; i < abiArgCount; ++i) {
if (args.isNaturalArg(i)) {
size_t naturalIndex = args.naturalIndex(i);
size_t stackIndex = naturalArgCount - 1 - naturalIndex + argsDepth;
passArg(argTypes[naturalIndex], peek(stackIndex), baselineCall);
} else {
// The synthetic stack result area pointer.
ABIArg argLoc = baselineCall->abi.next(MIRType::Pointer);
if (argLoc.kind() == ABIArg::Stack) {
ScratchPtr scratch(*this);
results.getStackResultArea(fr, scratch);
masm.storePtr(scratch, Address(masm.getStackPointer(),
argLoc.offsetFromArgBase()));
} else {
results.getStackResultArea(fr, RegPtr(argLoc.gpr()));
}
}
}
#ifndef RABALDR_PIN_INSTANCE
fr.loadInstancePtr(InstanceReg);
#endif
return true;
}
bool BaseCompiler::pushStackResultsForWasmCall(const ResultType& type,
RegPtr temp,
StackResultsLoc* loc) {
if (!ABIResultIter::HasStackResults(type)) {
return true;
}
// This method can increase stk_.length() by an unbounded amount, so we need
// to perform an allocation here to accomodate the variable number of values.
// There is enough headroom for any fixed number of values. The general case
// is handled in emitBody.
if (!stk_.reserve(stk_.length() + type.length())) {
return false;
}
// Measure stack results.
ABIResultIter i(type);
size_t count = 0;
for (; !i.done(); i.next()) {
if (i.cur().onStack()) {
count++;
}
}
uint32_t bytes = i.stackBytesConsumedSoFar();
// Reserve space for the stack results.
StackHeight resultsBase = fr.stackHeight();
uint32_t height = fr.prepareStackResultArea(resultsBase, bytes);
// Push Stk values onto the value stack, and zero out Ref values.
for (i.switchToPrev(); !i.done(); i.prev()) {
const ABIResult& result = i.cur();
if (result.onStack()) {
Stk v = captureStackResult(result, resultsBase, bytes);
push(v);
if (v.kind() == Stk::MemRef) {
stackMapGenerator_.memRefsOnStk++;
fr.storeImmediatePtrToStack(intptr_t(0), v.offs(), temp);
}
}
}
*loc = StackResultsLoc(bytes, count, height);
return true;
}
// After a call, some results may be written to the stack result locations that
// are pushed on the machine stack after any stack args. If there are stack
// args and stack results, these results need to be shuffled down, as the args
// are "consumed" by the call.
void BaseCompiler::popStackResultsAfterWasmCall(const StackResultsLoc& results,
uint32_t stackArgBytes) {
if (results.bytes() != 0) {
popValueStackBy(results.count());
if (stackArgBytes != 0) {
uint32_t srcHeight = results.height();
MOZ_ASSERT(srcHeight >= stackArgBytes + results.bytes());
uint32_t destHeight = srcHeight - stackArgBytes;
fr.shuffleStackResultsTowardFP(srcHeight, destHeight, results.bytes(),
ABINonArgReturnVolatileReg);
}
}
}
void BaseCompiler::pushBuiltinCallResult(const FunctionCall& call,
MIRType type) {
MOZ_ASSERT(call.abiKind == ABIKind::System);
switch (type) {
case MIRType::Int32: {
RegI32 rv = captureReturnedI32();
pushI32(rv);
break;
}
case MIRType::Int64: {
RegI64 rv = captureReturnedI64();
pushI64(rv);
break;
}
case MIRType::Float32: {
RegF32 rv = captureReturnedF32(call);
pushF32(rv);
break;
}
case MIRType::Double: {
RegF64 rv = captureReturnedF64(call);
pushF64(rv);
break;
}
#ifdef ENABLE_WASM_SIMD
case MIRType::Simd128: {
RegV128 rv = captureReturnedV128(call);
pushV128(rv);
break;
}
#endif
case MIRType::WasmAnyRef: {
RegRef rv = captureReturnedRef();
pushRef(rv);
break;
}
default:
// In particular, passing |type| as MIRType::Void or MIRType::Pointer to
// this function is an error.
MOZ_CRASH("Function return type");
}
}
bool BaseCompiler::pushWasmCallResults(const FunctionCall& call,
ResultType type,
const StackResultsLoc& loc) {
// pushResults currently bypasses special case code in captureReturnedFxx()
// that converts GPR results to FPR results for the system ABI when using
// softFP. If we ever start using that combination for calls we need more
// code.
MOZ_ASSERT(call.abiKind == ABIKind::Wasm);
return pushResults(type, fr.stackResultsBase(loc.bytes()));
}
CodeOffset BaseCompiler::callDefinition(uint32_t funcIndex,
const FunctionCall& call) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Func);
return masm.call(desc, funcIndex);
}
CodeOffset BaseCompiler::callSymbolic(SymbolicAddress callee,
const FunctionCall& call) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Symbolic);
return masm.call(desc, callee);
}
// Precondition: sync()
class OutOfLineAbortingTrap : public OutOfLineCode {
Trap trap_;
TrapSiteDesc desc_;
public:
OutOfLineAbortingTrap(Trap trap, const TrapSiteDesc& desc)
: trap_(trap), desc_(desc) {}
virtual void generate(MacroAssembler* masm) override {
masm->wasmTrap(trap_, desc_);
MOZ_ASSERT(!rejoin()->bound());
}
};
static ReturnCallAdjustmentInfo BuildReturnCallAdjustmentInfo(
const FuncType& callerType, const FuncType& calleeType) {
return ReturnCallAdjustmentInfo(
StackArgAreaSizeUnaligned(ArgTypeVector(calleeType), ABIKind::Wasm),
StackArgAreaSizeUnaligned(ArgTypeVector(callerType), ABIKind::Wasm));
}
bool BaseCompiler::callIndirect(uint32_t funcTypeIndex, uint32_t tableIndex,
const Stk& indexVal, const FunctionCall& call,
bool tailCall, CodeOffset* fastCallOffset,
CodeOffset* slowCallOffset) {
CallIndirectId callIndirectId =
CallIndirectId::forFuncType(codeMeta_, funcTypeIndex);
MOZ_ASSERT(callIndirectId.kind() != CallIndirectIdKind::AsmJS);
const TableDesc& table = codeMeta_.tables[tableIndex];
loadI32(indexVal, RegI32(WasmTableCallIndexReg));
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Indirect);
CalleeDesc callee =
CalleeDesc::wasmTable(codeMeta_, table, tableIndex, callIndirectId);
OutOfLineCode* oob = addOutOfLineCode(
new (alloc_) OutOfLineAbortingTrap(Trap::OutOfBounds, trapSiteDesc()));
if (!oob) {
return false;
}
Label* nullCheckFailed = nullptr;
#ifndef WASM_HAS_HEAPREG
OutOfLineCode* nullref = addOutOfLineCode(new (alloc_) OutOfLineAbortingTrap(
Trap::IndirectCallToNull, trapSiteDesc()));
if (!nullref) {
return false;
}
nullCheckFailed = nullref->entry();
#endif
if (!tailCall) {
masm.wasmCallIndirect(desc, callee, oob->entry(), nullCheckFailed,
mozilla::Nothing(), fastCallOffset, slowCallOffset);
} else {
ReturnCallAdjustmentInfo retCallInfo = BuildReturnCallAdjustmentInfo(
this->funcType(), (*codeMeta_.types)[funcTypeIndex].funcType());
masm.wasmReturnCallIndirect(desc, callee, oob->entry(), nullCheckFailed,
mozilla::Nothing(), retCallInfo);
}
return true;
}
class OutOfLineUpdateCallRefMetrics : public OutOfLineCode {
public:
virtual void generate(MacroAssembler* masm) override {
// Call the stub pointed to by Instance::updateCallRefMetricsStub, then
// rejoin. See "Register management" in BaseCompiler::updateCallRefMetrics
// for details of register management.
//
// The monitored call may or may not be cross-instance. The stub will only
// modify `regMetrics`, `regFuncRef`, `regScratch` and `regMetrics*` (that
// is, the pointed-at CallRefMetrics) and cannot fail or trap.
masm->call(
Address(InstanceReg, Instance::offsetOfUpdateCallRefMetricsStub()));
masm->jump(rejoin());
}
};
// Generate code that updates the `callRefIndex`th CallRefMetrics attached to
// the current Instance, to reflect the fact that this call site is just about
// to make a call to the funcref to which WasmCallRefReg currently points.
bool BaseCompiler::updateCallRefMetrics(size_t callRefIndex) {
AutoCreatedBy acb(masm, "BC::updateCallRefMetrics");
// See declaration of CallRefMetrics for comments about assignments of
// funcrefs to `CallRefMetrics::targets[]` fields.
// Register management: we will use three regs, `regMetrics`, `regFuncRef`,
// `regScratch` as detailed below. All of them may be trashed. But
// WasmCallRefReg needs to be unchanged across the update, so copy it into
// `regFuncRef` and use that instead of the original.
//
// At entry here, at entry to the OOL code, and at entry to the stub it
// calls, InstanceReg must be pointing at a valid Instance.
const Register regMetrics = WasmCallRefCallScratchReg0; // CallRefMetrics*
const Register regFuncRef = WasmCallRefCallScratchReg1; // FuncExtended*
const Register regScratch = WasmCallRefCallScratchReg2; // scratch
OutOfLineCode* ool =
addOutOfLineCode(new (alloc_) OutOfLineUpdateCallRefMetrics());
if (!ool) {
return false;
}
// We only need one Rejoin label, so use ool->rejoin() for that.
// if (target == nullptr) goto Rejoin
masm.branchWasmAnyRefIsNull(/*isNull=*/true, WasmCallRefReg, ool->rejoin());
// regFuncRef = target (make a copy of WasmCallRefReg)
masm.mov(WasmCallRefReg, regFuncRef);
// regMetrics = thisInstance::callRefMetrics_ + <imm>
//
// Emit a patchable mov32 which will load the offset of the `CallRefMetrics`
// stored inside the `Instance::callRefMetrics_` array
const CodeOffset offsetOfCallRefOffset = masm.move32WithPatch(regScratch);
masm.callRefMetricsPatches()[callRefIndex].setOffset(
offsetOfCallRefOffset.offset());
// Get a pointer to the `CallRefMetrics` for this call_ref
masm.loadPtr(Address(InstanceReg, wasm::Instance::offsetOfCallRefMetrics()),
regMetrics);
masm.addPtr(regScratch, regMetrics);
// At this point, regFuncRef = the FuncExtended*, regMetrics = the
// CallRefMetrics* and we know regFuncRef is not null.
//
// if (target->instance != thisInstance) goto Out-Of-Line
const size_t instanceSlotOffset = FunctionExtended::offsetOfExtendedSlot(
FunctionExtended::WASM_INSTANCE_SLOT);
masm.loadPtr(Address(regFuncRef, instanceSlotOffset), regScratch);
masm.branchPtr(Assembler::NotEqual, InstanceReg, regScratch, ool->entry());
// At this point, regFuncRef = the FuncExtended*, regMetrics = the
// CallRefMetrics*, we know regFuncRef is not null and it's a same-instance
// call.
//
// if (target != metrics->targets[0]) goto Out-Of-Line
const size_t offsetOfTarget0 = CallRefMetrics::offsetOfTarget(0);
masm.loadPtr(Address(regMetrics, offsetOfTarget0), regScratch);
masm.branchPtr(Assembler::NotEqual, regScratch, regFuncRef, ool->entry());
// At this point, regFuncRef = the FuncExtended*, regMetrics = the
// CallRefMetrics*, we know regFuncRef is not null, it's a same-instance
// call, and it is to the destination `regMetrics->targets[0]`.
//
// metrics->count0++
const size_t offsetOfCount0 = CallRefMetrics::offsetOfCount(0);
masm.load32(Address(regMetrics, offsetOfCount0), regScratch);
masm.add32(Imm32(1), regScratch);
masm.store32(regScratch, Address(regMetrics, offsetOfCount0));
masm.bind(ool->rejoin());
return true;
}
bool BaseCompiler::callRef(const Stk& calleeRef, const FunctionCall& call,
mozilla::Maybe<size_t> callRefIndex,
CodeOffset* fastCallOffset,
CodeOffset* slowCallOffset) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::FuncRef);
CalleeDesc callee = CalleeDesc::wasmFuncRef();
loadRef(calleeRef, RegRef(WasmCallRefReg));
if (compilerEnv_.mode() == CompileMode::LazyTiering) {
if (!updateCallRefMetrics(*callRefIndex)) {
return false;
}
} else {
MOZ_ASSERT(callRefIndex.isNothing());
}
masm.wasmCallRef(desc, callee, fastCallOffset, slowCallOffset);
return true;
}
void BaseCompiler::returnCallRef(const Stk& calleeRef, const FunctionCall& call,
const FuncType& funcType) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::FuncRef);
CalleeDesc callee = CalleeDesc::wasmFuncRef();
loadRef(calleeRef, RegRef(WasmCallRefReg));
ReturnCallAdjustmentInfo retCallInfo =
BuildReturnCallAdjustmentInfo(this->funcType(), funcType);
masm.wasmReturnCallRef(desc, callee, retCallInfo);
}
// Precondition: sync()
CodeOffset BaseCompiler::callImport(unsigned instanceDataOffset,
const FunctionCall& call) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Import);
CalleeDesc callee = CalleeDesc::import(instanceDataOffset);
return masm.wasmCallImport(desc, callee);
}
CodeOffset BaseCompiler::builtinCall(SymbolicAddress builtin,
const FunctionCall& call) {
return callSymbolic(builtin, call);
}
CodeOffset BaseCompiler::builtinInstanceMethodCall(
const SymbolicAddressSignature& builtin, const ABIArg& instanceArg,
const FunctionCall& call) {
#ifndef RABALDR_PIN_INSTANCE
// Builtin method calls assume the instance register has been set.
fr.loadInstancePtr(InstanceReg);
#endif
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Symbolic);
return masm.wasmCallBuiltinInstanceMethod(desc, instanceArg, builtin.identity,
builtin.failureMode,
builtin.failureTrap);
}
//////////////////////////////////////////////////////////////////////////////
//
// Exception handling
// Abstracted helper for throwing, used for throw, rethrow, and rethrowing
// at the end of a series of catch blocks (if none matched the exception).
bool BaseCompiler::throwFrom(RegRef exn) {
pushRef(exn);
// ThrowException invokes a trap, and the rest is dead code.
return emitInstanceCall(SASigThrowException);
}
void BaseCompiler::loadTag(RegPtr instance, uint32_t tagIndex, RegRef tagDst) {
size_t offset =
Instance::offsetInData(codeMeta_.offsetOfTagInstanceData(tagIndex));
masm.loadPtr(Address(instance, offset), tagDst);
}
void BaseCompiler::consumePendingException(RegPtr instance, RegRef* exnDst,
RegRef* tagDst) {
RegPtr pendingAddr = RegPtr(PreBarrierReg);
needPtr(pendingAddr);
masm.computeEffectiveAddress(
Address(instance, Instance::offsetOfPendingException()), pendingAddr);
*exnDst = needRef();
masm.loadPtr(Address(pendingAddr, 0), *exnDst);
emitBarrieredClear(pendingAddr);
*tagDst = needRef();
masm.computeEffectiveAddress(
Address(instance, Instance::offsetOfPendingExceptionTag()), pendingAddr);
masm.loadPtr(Address(pendingAddr, 0), *tagDst);
emitBarrieredClear(pendingAddr);
freePtr(pendingAddr);
}
bool BaseCompiler::startTryNote(size_t* tryNoteIndex) {
// Check the previous try note to ensure that we don't share an edge with
// it that could lead to ambiguity. Insert a nop, if required.
TryNoteVector& tryNotes = masm.tryNotes();
if (tryNotes.length() > 0) {
const TryNote& previous = tryNotes.back();
uint32_t currentOffset = masm.currentOffset();
if (previous.tryBodyBegin() == currentOffset ||
previous.tryBodyEnd() == currentOffset) {
masm.nop();
}
}
// Mark the beginning of the try note
wasm::TryNote tryNote = wasm::TryNote();
tryNote.setTryBodyBegin(masm.currentOffset());
return masm.append(tryNote, tryNoteIndex);
}
void BaseCompiler::finishTryNote(size_t tryNoteIndex) {
TryNoteVector& tryNotes = masm.tryNotes();
TryNote& tryNote = tryNotes[tryNoteIndex];
// Disallow zero-length try notes by inserting a no-op
if (tryNote.tryBodyBegin() == masm.currentOffset()) {
masm.nop();
}
// Check the most recent finished try note to ensure that we don't share an
// edge with it that could lead to ambiguity. Insert a nop, if required.
//
// Notice that finishTryNote is called in LIFO order -- using depth-first
// search numbering to see if we are traversing back from a nested try to a
// parent try, where we may need to ensure that the end offsets do not
// coincide.
//
// In the case the tryNodeIndex >= mostRecentFinishedTryNoteIndex_, we have
// finished a try that began after the most recent finished try, and so
// startTryNote will take care of any nops.
if (tryNoteIndex < mostRecentFinishedTryNoteIndex_) {
const TryNote& previous = tryNotes[mostRecentFinishedTryNoteIndex_];
uint32_t currentOffset = masm.currentOffset();
if (previous.tryBodyEnd() == currentOffset) {
masm.nop();
}
}
mostRecentFinishedTryNoteIndex_ = tryNoteIndex;
// Don't set the end of the try note if we've OOM'ed, as the above nop's may
// not have been placed. This is okay as this compilation will be thrown
// away.
if (masm.oom()) {
return;
}
// Mark the end of the try note
tryNote.setTryBodyEnd(masm.currentOffset());
}
////////////////////////////////////////////////////////////
//
// Platform-specific popping and register targeting.
// The simple popping methods pop values into targeted registers; the caller
// can free registers using standard functions. These are always called
// popXForY where X says something about types and Y something about the
// operation being targeted.
RegI32 BaseCompiler::needRotate64Temp() {
#if defined(JS_CODEGEN_X86)
return needI32();
#elif defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_ARM) || \
defined(JS_CODEGEN_ARM64) || defined(JS_CODEGEN_MIPS64) || \
defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_RISCV64)
return RegI32::Invalid();
#else
MOZ_CRASH("BaseCompiler platform hook: needRotate64Temp");
#endif
}
void BaseCompiler::popAndAllocateForDivAndRemI32(RegI32* r0, RegI32* r1,
RegI32* reserved) {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
// r0 must be eax, and edx will be clobbered.
need2xI32(specific_.eax, specific_.edx);
*r1 = popI32();
*r0 = popI32ToSpecific(specific_.eax);
*reserved = specific_.edx;
#else
pop2xI32(r0, r1);
#endif
}
static void QuotientI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd,
RegI32 reserved, IsUnsigned isUnsigned) {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
masm.quotient32(rs, rsd, reserved, isUnsigned);
#else
masm.quotient32(rs, rsd, isUnsigned);
#endif
}
static void RemainderI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd,
RegI32 reserved, IsUnsigned isUnsigned) {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
masm.remainder32(rs, rsd, reserved, isUnsigned);
#else
masm.remainder32(rs, rsd, isUnsigned);
#endif
}
void BaseCompiler::popAndAllocateForMulI64(RegI64* r0, RegI64* r1,
RegI32* temp) {
#if defined(JS_CODEGEN_X64)
pop2xI64(r0, r1);
#elif defined(JS_CODEGEN_X86)
// lhsDest must be edx:eax and rhs must not be that.
needI64(specific_.edx_eax);
*r1 = popI64();
*r0 = popI64ToSpecific(specific_.edx_eax);
*temp = needI32();
#elif defined(JS_CODEGEN_MIPS64)
pop2xI64(r0, r1);
#elif defined(JS_CODEGEN_ARM)
pop2xI64(r0, r1);
*temp = needI32();
#elif defined(JS_CODEGEN_ARM64)
pop2xI64(r0, r1);
#elif defined(JS_CODEGEN_LOONG64)
pop2xI64(r0, r1);
#elif defined(JS_CODEGEN_RISCV64)
pop2xI64(r0, r1);
#else
MOZ_CRASH("BaseCompiler porting interface: popAndAllocateForMulI64");
#endif
}
#ifndef RABALDR_INT_DIV_I64_CALLOUT
void BaseCompiler::popAndAllocateForDivAndRemI64(RegI64* r0, RegI64* r1,
RegI64* reserved,
IsRemainder isRemainder) {
# if defined(JS_CODEGEN_X64)
// r0 must be rax, and rdx will be clobbered.
need2xI64(specific_.rax, specific_.rdx);
*r1 = popI64();
*r0 = popI64ToSpecific(specific_.rax);
*reserved = specific_.rdx;
# elif defined(JS_CODEGEN_ARM64)
pop2xI64(r0, r1);
if (isRemainder) {
*reserved = needI64();
}
# else
pop2xI64(r0, r1);
# endif
}
static void QuotientI64(MacroAssembler& masm, RegI64 rhs, RegI64 srcDest,
RegI64 reserved, IsUnsigned isUnsigned) {
# if defined(JS_CODEGEN_X64)
// The caller must set up the following situation.
MOZ_ASSERT(srcDest.reg == rax);
MOZ_ASSERT(reserved.reg == rdx);
if (isUnsigned) {
masm.xorq(rdx, rdx);
masm.udivq(rhs.reg);
} else {
masm.cqo();
masm.idivq(rhs.reg);
}
# elif defined(JS_CODEGEN_MIPS64)
MOZ_ASSERT(reserved.isInvalid());
if (isUnsigned) {
masm.as_ddivu(srcDest.reg, rhs.reg);
} else {
masm.as_ddiv(srcDest.reg, rhs.reg);
}
masm.as_mflo(srcDest.reg);
# elif defined(JS_CODEGEN_ARM64)
MOZ_ASSERT(reserved.isInvalid());
ARMRegister sd(srcDest.reg, 64);
ARMRegister r(rhs.reg, 64);
if (isUnsigned) {
masm.Udiv(sd, sd, r);
} else {
masm.Sdiv(sd, sd, r);
}
# elif defined(JS_CODEGEN_LOONG64)
if (isUnsigned) {
masm.as_div_du(srcDest.reg, srcDest.reg, rhs.reg);
} else {
masm.as_div_d(srcDest.reg, srcDest.reg, rhs.reg);
}
# elif defined(JS_CODEGEN_RISCV64)
if (isUnsigned) {
masm.divu(srcDest.reg, srcDest.reg, rhs.reg);
} else {
masm.div(srcDest.reg, srcDest.reg, rhs.reg);
}
# else
MOZ_CRASH("BaseCompiler platform hook: quotientI64");
# endif
}
static void RemainderI64(MacroAssembler& masm, RegI64 rhs, RegI64 srcDest,
RegI64 reserved, IsUnsigned isUnsigned) {
# if defined(JS_CODEGEN_X64)
// The caller must set up the following situation.
MOZ_ASSERT(srcDest.reg == rax);
MOZ_ASSERT(reserved.reg == rdx);
if (isUnsigned) {
masm.xorq(rdx, rdx);
masm.udivq(rhs.reg);
} else {
masm.cqo();
masm.idivq(rhs.reg);
}
masm.movq(rdx, rax);
# elif defined(JS_CODEGEN_MIPS64)
MOZ_ASSERT(reserved.isInvalid());
if (isUnsigned) {
masm.as_ddivu(srcDest.reg, rhs.reg);
} else {
masm.as_ddiv(srcDest.reg, rhs.reg);
}
masm.as_mfhi(srcDest.reg);
# elif defined(JS_CODEGEN_ARM64)
ARMRegister sd(srcDest.reg, 64);
ARMRegister r(rhs.reg, 64);
ARMRegister t(reserved.reg, 64);
if (isUnsigned) {
masm.Udiv(t, sd, r);
} else {
masm.Sdiv(t, sd, r);
}
masm.Mul(t, t, r);
masm.Sub(sd, sd, t);
# elif defined(JS_CODEGEN_LOONG64)
if (isUnsigned) {
masm.as_mod_du(srcDest.reg, srcDest.reg, rhs.reg);
} else {
masm.as_mod_d(srcDest.reg, srcDest.reg, rhs.reg);
}
# elif defined(JS_CODEGEN_RISCV64)
if (isUnsigned) {
masm.remu(srcDest.reg, srcDest.reg, rhs.reg);
} else {
masm.rem(srcDest.reg, srcDest.reg, rhs.reg);
}
# else
MOZ_CRASH("BaseCompiler platform hook: remainderI64");
# endif
}
#endif // RABALDR_INT_DIV_I64_CALLOUT
RegI32 BaseCompiler::popI32RhsForShift() {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
// r1 must be ecx for a variable shift, unless BMI2 is available.
if (!Assembler::HasBMI2()) {
return popI32(specific_.ecx);
}
#endif
RegI32 r = popI32();
#if defined(JS_CODEGEN_ARM)
masm.and32(Imm32(31), r);
#endif
return r;
}
RegI32 BaseCompiler::popI32RhsForShiftI64() {
#if defined(JS_CODEGEN_X86)
// A limitation in the x86 masm requires ecx here
return popI32(specific_.ecx);
#elif defined(JS_CODEGEN_X64)
if (!Assembler::HasBMI2()) {
return popI32(specific_.ecx);
}
return popI32();
#else
return popI32();
#endif
}
RegI64 BaseCompiler::popI64RhsForShift() {
#if defined(JS_CODEGEN_X86)
// r1 must be ecx for a variable shift.
needI32(specific_.ecx);
return popI64ToSpecific(widenI32(specific_.ecx));
#else
# if defined(JS_CODEGEN_X64)
// r1 must be rcx for a variable shift, unless BMI2 is available.
if (!Assembler::HasBMI2()) {
needI64(specific_.rcx);
return popI64ToSpecific(specific_.rcx);
}
# endif
// No masking is necessary on 64-bit platforms, and on arm32 the masm
// implementation masks.
return popI64();
#endif
}
RegI32 BaseCompiler::popI32RhsForRotate() {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
// r1 must be ecx for a variable rotate.
return popI32(specific_.ecx);
#else
return popI32();
#endif
}
RegI64 BaseCompiler::popI64RhsForRotate() {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
// r1 must be ecx for a variable rotate.
needI32(specific_.ecx);
return popI64ToSpecific(widenI32(specific_.ecx));
#else
return popI64();
#endif
}
void BaseCompiler::popI32ForSignExtendI64(RegI64* r0) {
#if defined(JS_CODEGEN_X86)
// r0 must be edx:eax for cdq
need2xI32(specific_.edx, specific_.eax);
*r0 = specific_.edx_eax;
popI32ToSpecific(specific_.eax);
#else
*r0 = widenI32(popI32());
#endif
}
void BaseCompiler::popI64ForSignExtendI64(RegI64* r0) {
#if defined(JS_CODEGEN_X86)
// r0 must be edx:eax for cdq
need2xI32(specific_.edx, specific_.eax);
// Low on top, high underneath
*r0 = popI64ToSpecific(specific_.edx_eax);
#else
*r0 = popI64();
#endif
}
class OutOfLineTruncateCheckF32OrF64ToI32 : public OutOfLineCode {
AnyReg src;
RegI32 dest;
TruncFlags flags;
TrapSiteDesc trapSiteDesc;
public:
OutOfLineTruncateCheckF32OrF64ToI32(AnyReg src, RegI32 dest, TruncFlags flags,
TrapSiteDesc trapSiteDesc)
: src(src), dest(dest), flags(flags), trapSiteDesc(trapSiteDesc) {}
virtual void generate(MacroAssembler* masm) override {
if (src.tag == AnyReg::F32) {
masm->oolWasmTruncateCheckF32ToI32(src.f32(), dest, flags, trapSiteDesc,
rejoin());
} else if (src.tag == AnyReg::F64) {
masm->oolWasmTruncateCheckF64ToI32(src.f64(), dest, flags, trapSiteDesc,
rejoin());
} else {
MOZ_CRASH("unexpected type");
}
}
};
bool BaseCompiler::truncateF32ToI32(RegF32 src, RegI32 dest, TruncFlags flags) {
OutOfLineCode* ool =
addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI32(
AnyReg(src), dest, flags, trapSiteDesc()));
if (!ool) {
return false;
}
bool isSaturating = flags & TRUNC_SATURATING;
if (flags & TRUNC_UNSIGNED) {
masm.wasmTruncateFloat32ToUInt32(src, dest, isSaturating, ool->entry());
} else {
masm.wasmTruncateFloat32ToInt32(src, dest, isSaturating, ool->entry());
}
masm.bind(ool->rejoin());
return true;
}
bool BaseCompiler::truncateF64ToI32(RegF64 src, RegI32 dest, TruncFlags flags) {
OutOfLineCode* ool =
addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI32(
AnyReg(src), dest, flags, trapSiteDesc()));
if (!ool) {
return false;
}
bool isSaturating = flags & TRUNC_SATURATING;
if (flags & TRUNC_UNSIGNED) {
masm.wasmTruncateDoubleToUInt32(src, dest, isSaturating, ool->entry());
} else {
masm.wasmTruncateDoubleToInt32(src, dest, isSaturating, ool->entry());
}
masm.bind(ool->rejoin());
return true;
}
class OutOfLineTruncateCheckF32OrF64ToI64 : public OutOfLineCode {
AnyReg src;
RegI64 dest;
TruncFlags flags;
TrapSiteDesc trapSiteDesc;
public:
OutOfLineTruncateCheckF32OrF64ToI64(AnyReg src, RegI64 dest, TruncFlags flags,
TrapSiteDesc trapSiteDesc)
: src(src), dest(dest), flags(flags), trapSiteDesc(trapSiteDesc) {}
virtual void generate(MacroAssembler* masm) override {
if (src.tag == AnyReg::F32) {
masm->oolWasmTruncateCheckF32ToI64(src.f32(), dest, flags, trapSiteDesc,
rejoin());
} else if (src.tag == AnyReg::F64) {
masm->oolWasmTruncateCheckF64ToI64(src.f64(), dest, flags, trapSiteDesc,
rejoin());
} else {
MOZ_CRASH("unexpected type");
}
}
};
#ifndef RABALDR_FLOAT_TO_I64_CALLOUT
RegF64 BaseCompiler::needTempForFloatingToI64(TruncFlags flags) {
# if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
if (flags & TRUNC_UNSIGNED) {
return needF64();
}
# endif
return RegF64::Invalid();
}
bool BaseCompiler::truncateF32ToI64(RegF32 src, RegI64 dest, TruncFlags flags,
RegF64 temp) {
OutOfLineCode* ool =
addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(
AnyReg(src), dest, flags, trapSiteDesc()));
if (!ool) {
return false;
}
bool isSaturating = flags & TRUNC_SATURATING;
if (flags & TRUNC_UNSIGNED) {
masm.wasmTruncateFloat32ToUInt64(src, dest, isSaturating, ool->entry(),
ool->rejoin(), temp);
} else {
masm.wasmTruncateFloat32ToInt64(src, dest, isSaturating, ool->entry(),
ool->rejoin(), temp);
}
return true;
}
bool BaseCompiler::truncateF64ToI64(RegF64 src, RegI64 dest, TruncFlags flags,
RegF64 temp) {
OutOfLineCode* ool =
addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(
AnyReg(src), dest, flags, trapSiteDesc()));
if (!ool) {
return false;
}
bool isSaturating = flags & TRUNC_SATURATING;
if (flags & TRUNC_UNSIGNED) {
masm.wasmTruncateDoubleToUInt64(src, dest, isSaturating, ool->entry(),
ool->rejoin(), temp);
} else {
masm.wasmTruncateDoubleToInt64(src, dest, isSaturating, ool->entry(),
ool->rejoin(), temp);
}
return true;
}
#endif // RABALDR_FLOAT_TO_I64_CALLOUT
#ifndef RABALDR_I64_TO_FLOAT_CALLOUT
RegI32 BaseCompiler::needConvertI64ToFloatTemp(ValType to, bool isUnsigned) {
bool needs = false;
if (to == ValType::F64) {
needs = isUnsigned && masm.convertUInt64ToDoubleNeedsTemp();
} else {
# if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
needs = true;
# endif
}
return needs ? needI32() : RegI32::Invalid();
}
void BaseCompiler::convertI64ToF32(RegI64 src, bool isUnsigned, RegF32 dest,
RegI32 temp) {
if (isUnsigned) {
masm.convertUInt64ToFloat32(src, dest, temp);
} else {
masm.convertInt64ToFloat32(src, dest);
}
}
void BaseCompiler::convertI64ToF64(RegI64 src, bool isUnsigned, RegF64 dest,
RegI32 temp) {
if (isUnsigned) {
masm.convertUInt64ToDouble(src, dest, temp);
} else {
masm.convertInt64ToDouble(src, dest);
}
}
#endif // RABALDR_I64_TO_FLOAT_CALLOUT
//////////////////////////////////////////////////////////////////////
//
// Global variable access.
Address BaseCompiler::addressOfGlobalVar(const GlobalDesc& global, RegPtr tmp) {
uint32_t globalToInstanceOffset = Instance::offsetInData(global.offset());
#ifdef RABALDR_PIN_INSTANCE
movePtr(RegPtr(InstanceReg), tmp);
#else
fr.loadInstancePtr(tmp);
#endif
if (global.isIndirect()) {
masm.loadPtr(Address(tmp, globalToInstanceOffset), tmp);
return Address(tmp, 0);
}
return Address(tmp, globalToInstanceOffset);
}
//////////////////////////////////////////////////////////////////////
//
// Table access.
Address BaseCompiler::addressOfTableField(uint32_t tableIndex,
uint32_t fieldOffset,
RegPtr instance) {
uint32_t tableToInstanceOffset = wasm::Instance::offsetInData(
codeMeta_.offsetOfTableInstanceData(tableIndex) + fieldOffset);
return Address(instance, tableToInstanceOffset);
}
void BaseCompiler::loadTableLength(uint32_t tableIndex, RegPtr instance,
RegI32 length) {
masm.load32(addressOfTableField(
tableIndex, offsetof(TableInstanceData, length), instance),
length);
}
void BaseCompiler::loadTableElements(uint32_t tableIndex, RegPtr instance,
RegPtr elements) {
masm.loadPtr(addressOfTableField(
tableIndex, offsetof(TableInstanceData, elements), instance),
elements);
}
//////////////////////////////////////////////////////////////////////////////
//
// Basic emitters for simple operators.
static void AddI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.add32(rs, rsd);
}
static void AddImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.add32(Imm32(c), rsd);
}
static void SubI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.sub32(rs, rsd);
}
static void SubImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.sub32(Imm32(c), rsd);
}
static void MulI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.mul32(rs, rsd);
}
static void OrI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.or32(rs, rsd);
}
static void OrImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.or32(Imm32(c), rsd);
}
static void AndI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.and32(rs, rsd);
}
static void AndImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.and32(Imm32(c), rsd);
}
static void XorI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.xor32(rs, rsd);
}
static void XorImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.xor32(Imm32(c), rsd);
}
static void ClzI32(MacroAssembler& masm, RegI32 rsd) {
masm.clz32(rsd, rsd, IsKnownNotZero(false));
}
static void CtzI32(MacroAssembler& masm, RegI32 rsd) {
masm.ctz32(rsd, rsd, IsKnownNotZero(false));
}
// Currently common to PopcntI32 and PopcntI64
static RegI32 PopcntTemp(BaseCompiler& bc) {
#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
return AssemblerX86Shared::HasPOPCNT() ? RegI32::Invalid() : bc.needI32();
#elif defined(JS_CODEGEN_ARM) || defined(JS_CODEGEN_ARM64) || \
defined(JS_CODEGEN_MIPS64) || defined(JS_CODEGEN_LOONG64) || \
defined(JS_CODEGEN_RISCV64)
return bc.needI32();
#else
MOZ_CRASH("BaseCompiler platform hook: PopcntTemp");
#endif
}
static void PopcntI32(BaseCompiler& bc, RegI32 rsd, RegI32 temp) {
bc.masm.popcnt32(rsd, rsd, temp);
}
static void ShlI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.lshift32(rs, rsd);
}
static void ShlImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.lshift32(Imm32(c & 31), rsd);
}
static void ShrI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.rshift32Arithmetic(rs, rsd);
}
static void ShrImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.rshift32Arithmetic(Imm32(c & 31), rsd);
}
static void ShrUI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.rshift32(rs, rsd);
}
static void ShrUImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.rshift32(Imm32(c & 31), rsd);
}
static void RotlI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.rotateLeft(rs, rsd, rsd);
}
static void RotlImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.rotateLeft(Imm32(c & 31), rsd, rsd);
}
static void RotrI32(MacroAssembler& masm, RegI32 rs, RegI32 rsd) {
masm.rotateRight(rs, rsd, rsd);
}
static void RotrImmI32(MacroAssembler& masm, int32_t c, RegI32 rsd) {
masm.rotateRight(Imm32(c & 31), rsd, rsd);
}
static void EqzI32(MacroAssembler& masm, RegI32 rsd) {
masm.cmp32Set(Assembler::Equal, rsd, Imm32(0), rsd);
}
static void WrapI64ToI32(MacroAssembler& masm, RegI64 rs, RegI32 rd) {
masm.move64To32(rs, rd);
}
static void AddI64(MacroAssembler& masm, RegI64 rs, RegI64 rsd) {
masm.add64(rs, rsd);
}
static void AddImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.add64(Imm64(c), rsd);
}
static void SubI64(MacroAssembler& masm, RegI64 rs, RegI64 rsd) {
masm.sub64(rs, rsd);
}
static void SubImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.sub64(Imm64(c), rsd);
}
static void OrI64(MacroAssembler& masm, RegI64 rs, RegI64 rsd) {
masm.or64(rs, rsd);
}
static void OrImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.or64(Imm64(c), rsd);
}
static void AndI64(MacroAssembler& masm, RegI64 rs, RegI64 rsd) {
masm.and64(rs, rsd);
}
static void AndImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.and64(Imm64(c), rsd);
}
static void XorI64(MacroAssembler& masm, RegI64 rs, RegI64 rsd) {
masm.xor64(rs, rsd);
}
static void XorImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.xor64(Imm64(c), rsd);
}
static void ClzI64(BaseCompiler& bc, RegI64 rsd) { bc.masm.clz64(rsd, rsd); }
static void CtzI64(BaseCompiler& bc, RegI64 rsd) { bc.masm.ctz64(rsd, rsd); }
static void PopcntI64(BaseCompiler& bc, RegI64 rsd, RegI32 temp) {
bc.masm.popcnt64(rsd, rsd, temp);
}
static void ShlI64(BaseCompiler& bc, RegI64 rs, RegI64 rsd) {
bc.masm.lshift64(bc.lowPart(rs), rsd);
}
static void ShlImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.lshift64(Imm32(c & 63), rsd);
}
static void ShrI64(BaseCompiler& bc, RegI64 rs, RegI64 rsd) {
bc.masm.rshift64Arithmetic(bc.lowPart(rs), rsd);
}
static void ShrImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.rshift64Arithmetic(Imm32(c & 63), rsd);
}
static void ShrUI64(BaseCompiler& bc, RegI64 rs, RegI64 rsd) {
bc.masm.rshift64(bc.lowPart(rs), rsd);
}
static void ShrUImmI64(MacroAssembler& masm, int64_t c, RegI64 rsd) {
masm.rshift64(Imm32(c & 63), rsd);
}
static void EqzI64(MacroAssembler& masm, RegI64 rs, RegI32 rd) {
#ifdef JS_PUNBOX64
masm.cmpPtrSet(Assembler::Equal, rs.reg, ImmWord(0), rd);
#else
MOZ_ASSERT(rs.low == rd);
masm.or32(rs.high, rs.low);
masm.cmp32Set(Assembler::Equal, rs.low, Imm32(0), rd);
#endif
}
static void AddF64(MacroAssembler& masm, RegF64 rs, RegF64 rsd) {
masm.addDouble(rs, rsd);
}
static void SubF64(MacroAssembler& masm, RegF64 rs, RegF64 rsd) {
masm.subDouble(rs, rsd);
}
static void MulF64(MacroAssembler& masm, RegF64 rs, RegF64 rsd) {
masm.mulDouble(rs, rsd);
}
static void DivF64(MacroAssembler& masm, RegF64 rs, RegF64 rsd) {
masm.divDouble(rs, rsd);
}
static void MinF64(BaseCompiler& bc, RegF64 rs, RegF64 rsd) {
// Convert signaling NaN to quiet NaNs.
//
// TODO / OPTIMIZE (bug 1316824): see comment in MinF32.
#ifdef RABALDR_SCRATCH_F64
ScratchF64 zero(bc.ra);
#else
ScratchF64 zero(bc.masm);
#endif
bc.masm.loadConstantDouble(0, zero);
bc.masm.subDouble(zero, rsd);
bc.masm.subDouble(zero, rs);
bc.masm.minDouble(rs, rsd, HandleNaNSpecially(true));
}
static void MaxF64(BaseCompiler& bc, RegF64 rs, RegF64 rsd) {
// Convert signaling NaN to quiet NaNs.
//
// TODO / OPTIMIZE (bug 1316824): see comment in MinF32.
#ifdef RABALDR_SCRATCH_F64
ScratchF64 zero(bc.ra);
#else
ScratchF64 zero(bc.masm);
#endif
bc.masm.loadConstantDouble(0, zero);
bc.masm.subDouble(zero, rsd);
bc.masm.subDouble(zero, rs);
bc.masm.maxDouble(rs, rsd, HandleNaNSpecially(true));
}
static void CopysignF64(MacroAssembler& masm, RegF64 rs, RegF64 rsd,
RegI64 temp0, RegI64 temp1) {
masm.moveDoubleToGPR64(rsd, temp0);
masm.moveDoubleToGPR64(rs, temp1);
masm.and64(Imm64(INT64_MAX), temp0);
masm.and64(Imm64(INT64_MIN), temp1);
masm.or64(temp1, temp0);
masm.moveGPR64ToDouble(temp0, rsd);
}
static void AbsF64(MacroAssembler& masm, RegF64 rsd) {
masm.absDouble(rsd, rsd);
}
static void NegateF64(MacroAssembler& masm, RegF64 rsd) {
masm.negateDouble(rsd);
}
static void SqrtF64(MacroAssembler& masm, RegF64 rsd) {
masm.sqrtDouble(rsd, rsd);
}
static void AddF32(MacroAssembler& masm, RegF32 rs, RegF32 rsd) {
masm.addFloat32(rs, rsd);
}
static void SubF32(MacroAssembler& masm, RegF32 rs, RegF32 rsd) {
masm.subFloat32(rs, rsd);
}
static void MulF32(MacroAssembler& masm, RegF32 rs, RegF32 rsd) {
masm.mulFloat32(rs, rsd);
}
static void DivF32(MacroAssembler& masm, RegF32 rs, RegF32 rsd) {
masm.divFloat32(rs, rsd);
}
static void MinF32(BaseCompiler& bc, RegF32 rs, RegF32 rsd) {
// Convert signaling NaN to quiet NaNs.
//
// TODO / OPTIMIZE (bug 1316824): Don't do this if one of the operands
// is known to be a constant.
#ifdef RABALDR_SCRATCH_F32
ScratchF32 zero(bc.ra);
#else
ScratchF32 zero(bc.masm);
#endif
bc.masm.loadConstantFloat32(0.f, zero);
bc.masm.subFloat32(zero, rsd);
bc.masm.subFloat32(zero, rs);
bc.masm.minFloat32(rs, rsd, HandleNaNSpecially(true));
}
static void MaxF32(BaseCompiler& bc, RegF32 rs, RegF32 rsd) {
// Convert signaling NaN to quiet NaNs.
//
// TODO / OPTIMIZE (bug 1316824): see comment in MinF32.
#ifdef RABALDR_SCRATCH_F32
ScratchF32 zero(bc.ra);
#else
ScratchF32 zero(bc.masm);
#endif
bc.masm.loadConstantFloat32(0.f, zero);
bc.masm.subFloat32(zero, rsd);
bc.masm.subFloat32(zero, rs);
bc.masm.maxFloat32(rs, rsd, HandleNaNSpecially(true));
}
static void CopysignF32(MacroAssembler& masm, RegF32 rs, RegF32 rsd,
RegI32 temp0, RegI32 temp1) {
masm.moveFloat32ToGPR(rsd, temp0);
masm.moveFloat32ToGPR(rs, temp1);
masm.and32(Imm32(INT32_MAX), temp0);
masm.and32(Imm32(INT32_MIN), temp1);
masm.or32(temp1, temp0);
masm.moveGPRToFloat32(temp0, rsd);
}
static void AbsF32(MacroAssembler& masm, RegF32 rsd) {
masm.absFloat32(rsd, rsd);
}
static void NegateF32(MacroAssembler& masm, RegF32 rsd) {
masm.negateFloat(rsd);
}
static void SqrtF32(MacroAssembler& masm, RegF32 rsd) {
masm.sqrtFloat32(rsd, rsd);
}
#ifndef RABALDR_I64_TO_FLOAT_CALLOUT
static void ConvertI64ToF32(MacroAssembler& masm, RegI64 rs, RegF32 rd) {
masm.convertInt64ToFloat32(rs, rd);
}
static void ConvertI64ToF64(MacroAssembler& masm, RegI64 rs, RegF64 rd) {
masm.convertInt64ToDouble(rs, rd);
}
#endif
static void ReinterpretF32AsI32(MacroAssembler& masm, RegF32 rs, RegI32 rd) {
masm.moveFloat32ToGPR(rs, rd);
}
static void ReinterpretF64AsI64(MacroAssembler& masm, RegF64 rs, RegI64 rd) {
masm.moveDoubleToGPR64(rs, rd);
}
static void ConvertF64ToF32(MacroAssembler& masm, RegF64 rs, RegF32 rd) {
masm.convertDoubleToFloat32(rs, rd);
}
static void ConvertI32ToF32(MacroAssembler& masm, RegI32 rs, RegF32 rd) {
masm.convertInt32ToFloat32(rs, rd);
}
static void ConvertU32ToF32(MacroAssembler& masm, RegI32 rs, RegF32 rd) {
masm.convertUInt32ToFloat32(rs, rd);
}
static void ConvertF32ToF64(MacroAssembler& masm, RegF32 rs, RegF64 rd) {
masm.convertFloat32ToDouble(rs, rd);
}
static void ConvertI32ToF64(MacroAssembler& masm, RegI32 rs, RegF64 rd) {
masm.convertInt32ToDouble(rs, rd);
}
static void ConvertU32ToF64(MacroAssembler& masm, RegI32 rs, RegF64 rd) {
masm.convertUInt32ToDouble(rs, rd);
}
static void ReinterpretI32AsF32(MacroAssembler& masm, RegI32 rs, RegF32 rd) {
masm.moveGPRToFloat32(rs, rd);
}
static void ReinterpretI64AsF64(MacroAssembler& masm, RegI64 rs, RegF64 rd) {
masm.moveGPR64ToDouble(rs, rd);
}
static void ExtendI32_8(BaseCompiler& bc, RegI32 rsd) {
#ifdef JS_CODEGEN_X86
if (!bc.ra.isSingleByteI32(rsd)) {
ScratchI8 scratch(bc.ra);
bc.masm.move32(rsd, scratch);
bc.masm.move8SignExtend(scratch, rsd);
return;
}
#endif
bc.masm.move8SignExtend(rsd, rsd);
}
static void ExtendI32_16(MacroAssembler& masm, RegI32 rsd) {
masm.move16SignExtend(rsd, rsd);
}
void BaseCompiler::emitMultiplyI64() {
RegI64 r, rs;
RegI32 temp;
popAndAllocateForMulI64(&r, &rs, &temp);
masm.mul64(rs, r, temp);
maybeFree(temp);
freeI64(rs);
pushI64(r);
}
template <typename RegType, typename IntType>
void BaseCompiler::quotientOrRemainder(
RegType rs, RegType rsd, RegType reserved, IsUnsigned isUnsigned,
ZeroOnOverflow zeroOnOverflow, bool isConst, IntType c,
void (*operate)(MacroAssembler& masm, RegType rs, RegType rsd,
RegType reserved, IsUnsigned isUnsigned)) {
Label done;
if (!isConst || c == 0) {
checkDivideByZero(rs);
}
if (!isUnsigned && (!isConst || c == -1)) {
checkDivideSignedOverflow(rs, rsd, &done, zeroOnOverflow);
}
operate(masm, rs, rsd, reserved, isUnsigned);
masm.bind(&done);
}
void BaseCompiler::emitQuotientI32() {
int32_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 0)) {
if (power != 0) {
RegI32 r = popI32();
Label positive;
masm.branchTest32(Assembler::NotSigned, r, r, &positive);
masm.add32(Imm32(c - 1), r);
masm.bind(&positive);
masm.rshift32Arithmetic(Imm32(power & 31), r);
pushI32(r);
}
} else {
bool isConst = peekConst(&c);
RegI32 r, rs, reserved;
popAndAllocateForDivAndRemI32(&r, &rs, &reserved);
quotientOrRemainder(rs, r, reserved, IsUnsigned(false),
ZeroOnOverflow(false), isConst, c, QuotientI32);
maybeFree(reserved);
freeI32(rs);
pushI32(r);
}
}
void BaseCompiler::emitQuotientU32() {
int32_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 0)) {
if (power != 0) {
RegI32 r = popI32();
masm.rshift32(Imm32(power & 31), r);
pushI32(r);
}
} else {
bool isConst = peekConst(&c);
RegI32 r, rs, reserved;
popAndAllocateForDivAndRemI32(&r, &rs, &reserved);
quotientOrRemainder(rs, r, reserved, IsUnsigned(true),
ZeroOnOverflow(false), isConst, c, QuotientI32);
maybeFree(reserved);
freeI32(rs);
pushI32(r);
}
}
void BaseCompiler::emitRemainderI32() {
int32_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 1)) {
RegI32 r = popI32();
RegI32 temp = needI32();
moveI32(r, temp);
Label positive;
masm.branchTest32(Assembler::NotSigned, temp, temp, &positive);
masm.add32(Imm32(c - 1), temp);
masm.bind(&positive);
masm.rshift32Arithmetic(Imm32(power & 31), temp);
masm.lshift32(Imm32(power & 31), temp);
masm.sub32(temp, r);
freeI32(temp);
pushI32(r);
} else {
bool isConst = peekConst(&c);
RegI32 r, rs, reserved;
popAndAllocateForDivAndRemI32(&r, &rs, &reserved);
quotientOrRemainder(rs, r, reserved, IsUnsigned(false),
ZeroOnOverflow(true), isConst, c, RemainderI32);
maybeFree(reserved);
freeI32(rs);
pushI32(r);
}
}
void BaseCompiler::emitRemainderU32() {
int32_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 1)) {
RegI32 r = popI32();
masm.and32(Imm32(c - 1), r);
pushI32(r);
} else {
bool isConst = peekConst(&c);
RegI32 r, rs, reserved;
popAndAllocateForDivAndRemI32(&r, &rs, &reserved);
quotientOrRemainder(rs, r, reserved, IsUnsigned(true), ZeroOnOverflow(true),
isConst, c, RemainderI32);
maybeFree(reserved);
freeI32(rs);
pushI32(r);
}
}
#ifndef RABALDR_INT_DIV_I64_CALLOUT
void BaseCompiler::emitQuotientI64() {
int64_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 0)) {
if (power != 0) {
RegI64 r = popI64();
Label positive;
masm.branchTest64(Assembler::NotSigned, r, r, &positive);
masm.add64(Imm64(c - 1), r);
masm.bind(&positive);
masm.rshift64Arithmetic(Imm32(power & 63), r);
pushI64(r);
}
} else {
bool isConst = peekConst(&c);
RegI64 r, rs, reserved;
popAndAllocateForDivAndRemI64(&r, &rs, &reserved, IsRemainder(false));
quotientOrRemainder(rs, r, reserved, IsUnsigned(false),
ZeroOnOverflow(false), isConst, c, QuotientI64);
maybeFree(reserved);
freeI64(rs);
pushI64(r);
}
}
void BaseCompiler::emitQuotientU64() {
int64_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 0)) {
if (power != 0) {
RegI64 r = popI64();
masm.rshift64(Imm32(power & 63), r);
pushI64(r);
}
} else {
bool isConst = peekConst(&c);
RegI64 r, rs, reserved;
popAndAllocateForDivAndRemI64(&r, &rs, &reserved, IsRemainder(false));
quotientOrRemainder(rs, r, reserved, IsUnsigned(true),
ZeroOnOverflow(false), isConst, c, QuotientI64);
maybeFree(reserved);
freeI64(rs);
pushI64(r);
}
}
void BaseCompiler::emitRemainderI64() {
int64_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 1)) {
RegI64 r = popI64();
RegI64 temp = needI64();
moveI64(r, temp);
Label positive;
masm.branchTest64(Assembler::NotSigned, temp, temp, &positive);
masm.add64(Imm64(c - 1), temp);
masm.bind(&positive);
masm.rshift64Arithmetic(Imm32(power & 63), temp);
masm.lshift64(Imm32(power & 63), temp);
masm.sub64(temp, r);
freeI64(temp);
pushI64(r);
} else {
bool isConst = peekConst(&c);
RegI64 r, rs, reserved;
popAndAllocateForDivAndRemI64(&r, &rs, &reserved, IsRemainder(true));
quotientOrRemainder(rs, r, reserved, IsUnsigned(false),
ZeroOnOverflow(true), isConst, c, RemainderI64);
maybeFree(reserved);
freeI64(rs);
pushI64(r);
}
}
void BaseCompiler::emitRemainderU64() {
int64_t c;
uint_fast8_t power;
if (popConstPositivePowerOfTwo(&c, &power, 1)) {
RegI64 r = popI64();
masm.and64(Imm64(c - 1), r);
pushI64(r);
} else {
bool isConst = peekConst(&c);
RegI64 r, rs, reserved;
popAndAllocateForDivAndRemI64(&r, &rs, &reserved, IsRemainder(true));
quotientOrRemainder(rs, r, reserved, IsUnsigned(true), ZeroOnOverflow(true),
isConst, c, RemainderI64);
maybeFree(reserved);
freeI64(rs);
pushI64(r);
}
}
#endif // RABALDR_INT_DIV_I64_CALLOUT
void BaseCompiler::emitRotrI64() {
int64_t c;
if (popConst(&c)) {
RegI64 r = popI64();
RegI32 temp = needRotate64Temp();
masm.rotateRight64(Imm32(c & 63), r, r, temp);
maybeFree(temp);
pushI64(r);
} else {
RegI64 rs = popI64RhsForRotate();
RegI64 r = popI64();
masm.rotateRight64(lowPart(rs), r, r, maybeHighPart(rs));
freeI64(rs);
pushI64(r);
}
}
void BaseCompiler::emitRotlI64() {
int64_t c;
if (popConst(&c)) {
RegI64 r = popI64();
RegI32 temp = needRotate64Temp();
masm.rotateLeft64(Imm32(c & 63), r, r, temp);
maybeFree(temp);
pushI64(r);
} else {
RegI64 rs = popI64RhsForRotate();
RegI64 r = popI64();
masm.rotateLeft64(lowPart(rs), r, r, maybeHighPart(rs));
freeI64(rs);
pushI64(r);
}
}
void BaseCompiler::emitEqzI32() {
if (sniffConditionalControlEqz(ValType::I32)) {
return;
}
emitUnop(EqzI32);
}
void BaseCompiler::emitEqzI64() {
if (sniffConditionalControlEqz(ValType::I64)) {
return;
}
emitUnop(EqzI64);
}
template <TruncFlags flags>
bool BaseCompiler::emitTruncateF32ToI32() {
RegF32 rs = popF32();
RegI32 rd = needI32();
if (!truncateF32ToI32(rs, rd, flags)) {
return false;
}
freeF32(rs);
pushI32(rd);
return true;
}
template <TruncFlags flags>
bool BaseCompiler::emitTruncateF64ToI32() {
RegF64 rs = popF64();
RegI32 rd = needI32();
if (!truncateF64ToI32(rs, rd, flags)) {
return false;
}
freeF64(rs);
pushI32(rd);
return true;
}
#ifndef RABALDR_FLOAT_TO_I64_CALLOUT
template <TruncFlags flags>
bool BaseCompiler::emitTruncateF32ToI64() {
RegF32 rs = popF32();
RegI64 rd = needI64();
RegF64 temp = needTempForFloatingToI64(flags);
if (!truncateF32ToI64(rs, rd, flags, temp)) {
return false;
}
maybeFree(temp);
freeF32(rs);
pushI64(rd);
return true;
}
template <TruncFlags flags>
bool BaseCompiler::emitTruncateF64ToI64() {
RegF64 rs = popF64();
RegI64 rd = needI64();
RegF64 temp = needTempForFloatingToI64(flags);
if (!truncateF64ToI64(rs, rd, flags, temp)) {
return false;
}
maybeFree(temp);
freeF64(rs);
pushI64(rd);
return true;
}
#endif // RABALDR_FLOAT_TO_I64_CALLOUT
void BaseCompiler::emitExtendI64_8() {
RegI64 r;
popI64ForSignExtendI64(&r);
masm.move8To64SignExtend(lowPart(r), r);
pushI64(r);
}
void BaseCompiler::emitExtendI64_16() {
RegI64 r;
popI64ForSignExtendI64(&r);
masm.move16To64SignExtend(lowPart(r), r);
pushI64(r);
}
void BaseCompiler::emitExtendI64_32() {
RegI64 r;
popI64ForSignExtendI64(&r);
masm.move32To64SignExtend(lowPart(r), r);
pushI64(r);
}
void BaseCompiler::emitExtendI32ToI64() {
RegI64 r;
popI32ForSignExtendI64(&r);
masm.move32To64SignExtend(lowPart(r), r);
pushI64(r);
}
void BaseCompiler::emitExtendU32ToI64() {
RegI32 rs = popI32();
RegI64 rd = widenI32(rs);
masm.move32To64ZeroExtend(rs, rd);
pushI64(rd);
}
#ifndef RABALDR_I64_TO_FLOAT_CALLOUT
void BaseCompiler::emitConvertU64ToF32() {
RegI64 rs = popI64();
RegF32 rd = needF32();
RegI32 temp = needConvertI64ToFloatTemp(ValType::F32, IsUnsigned(true));
convertI64ToF32(rs, IsUnsigned(true), rd, temp);
maybeFree(temp);
freeI64(rs);
pushF32(rd);
}
void BaseCompiler::emitConvertU64ToF64() {
RegI64 rs = popI64();
RegF64 rd = needF64();
RegI32 temp = needConvertI64ToFloatTemp(ValType::F64, IsUnsigned(true));
convertI64ToF64(rs, IsUnsigned(true), rd, temp);
maybeFree(temp);
freeI64(rs);
pushF64(rd);
}
#endif // RABALDR_I64_TO_FLOAT_CALLOUT
////////////////////////////////////////////////////////////
//
// Machinery for optimized conditional branches.
//
// To disable this optimization it is enough always to return false from
// sniffConditionalControl{Cmp,Eqz}.
struct BranchState {
union {
struct {
RegI32 lhs;
RegI32 rhs;
int32_t imm;
bool rhsImm;
} i32;
struct {
RegI64 lhs;
RegI64 rhs;
int64_t imm;
bool rhsImm;
} i64;
struct {
RegF32 lhs;
RegF32 rhs;
} f32;
struct {
RegF64 lhs;
RegF64 rhs;
} f64;
};
Label* const label; // The target of the branch, never NULL
const StackHeight stackHeight; // The stack base above which to place
// stack-spilled block results, if
// hasBlockResults().
const bool invertBranch; // If true, invert the sense of the branch
const ResultType resultType; // The result propagated along the edges
explicit BranchState(Label* label)
: label(label),
stackHeight(StackHeight::Invalid()),
invertBranch(false),
resultType(ResultType::Empty()) {}
BranchState(Label* label, bool invertBranch)
: label(label),
stackHeight(StackHeight::Invalid()),
invertBranch(invertBranch),
resultType(ResultType::Empty()) {}
BranchState(Label* label, StackHeight stackHeight, bool invertBranch,
ResultType resultType)
: label(label),
stackHeight(stackHeight),
invertBranch(invertBranch),
resultType(resultType) {}
bool hasBlockResults() const { return stackHeight.isValid(); }
};
void BaseCompiler::setLatentCompare(Assembler::Condition compareOp,
ValType operandType) {
latentOp_ = LatentOp::Compare;
latentType_ = operandType;
latentIntCmp_ = compareOp;
}
void BaseCompiler::setLatentCompare(Assembler::DoubleCondition compareOp,
ValType operandType) {
latentOp_ = LatentOp::Compare;
latentType_ = operandType;
latentDoubleCmp_ = compareOp;
}
void BaseCompiler::setLatentEqz(ValType operandType) {
latentOp_ = LatentOp::Eqz;
latentType_ = operandType;
}
bool BaseCompiler::hasLatentOp() const { return latentOp_ != LatentOp::None; }
void BaseCompiler::resetLatentOp() { latentOp_ = LatentOp::None; }
// Emit a conditional branch that optionally and optimally cleans up the CPU
// stack before we branch.
//
// Cond is either Assembler::Condition or Assembler::DoubleCondition.
//
// Lhs is RegI32, RegI64, or RegF32, RegF64, or RegRef.
//
// Rhs is either the same as Lhs, or an immediate expression compatible with
// Lhs "when applicable".
template <typename Cond, typename Lhs, typename Rhs>
bool BaseCompiler::jumpConditionalWithResults(BranchState* b, Cond cond,
Lhs lhs, Rhs rhs) {
if (b->hasBlockResults()) {
StackHeight resultsBase(0);
if (!topBranchParams(b->resultType, &resultsBase)) {
return false;
}
if (b->stackHeight != resultsBase) {
Label notTaken;
branchTo(b->invertBranch ? cond : Assembler::InvertCondition(cond), lhs,
rhs, &notTaken);
// Shuffle stack args.
shuffleStackResultsBeforeBranch(resultsBase, b->stackHeight,
b->resultType);
masm.jump(b->label);
masm.bind(&notTaken);
return true;
}
}
branchTo(b->invertBranch ? Assembler::InvertCondition(cond) : cond, lhs, rhs,
b->label);
return true;
}
bool BaseCompiler::jumpConditionalWithResults(BranchState* b, RegRef object,
MaybeRefType sourceType,
RefType destType,
bool onSuccess) {
// Temporarily take the result registers so that branchIfRefSubtype
// doesn't use them.
needIntegerResultRegisters(b->resultType);
BranchIfRefSubtypeRegisters regs =
allocRegistersForBranchIfRefSubtype(destType);
freeIntegerResultRegisters(b->resultType);
if (b->hasBlockResults()) {
StackHeight resultsBase(0);
if (!topBranchParams(b->resultType, &resultsBase)) {
return false;
}
if (b->stackHeight != resultsBase) {
Label notTaken;
masm.branchWasmRefIsSubtype(
object, sourceType, destType, &notTaken,
/*onSuccess=*/b->invertBranch ? onSuccess : !onSuccess, regs.superSTV,
regs.scratch1, regs.scratch2);
freeRegistersForBranchIfRefSubtype(regs);
// Shuffle stack args.
shuffleStackResultsBeforeBranch(resultsBase, b->stackHeight,
b->resultType);
masm.jump(b->label);
masm.bind(&notTaken);
return true;
}
}
masm.branchWasmRefIsSubtype(
object, sourceType, destType, b->label,
/*onSuccess=*/b->invertBranch ? !onSuccess : onSuccess, regs.superSTV,
regs.scratch1, regs.scratch2);
freeRegistersForBranchIfRefSubtype(regs);
return true;
}
// sniffConditionalControl{Cmp,Eqz} may modify the latentWhatever_ state in
// the BaseCompiler so that a subsequent conditional branch can be compiled
// optimally. emitBranchSetup() and emitBranchPerform() will consume that
// state. If the latter methods are not called because deadCode_ is true
// then the compiler MUST instead call resetLatentOp() to reset the state.
template <typename Cond>
bool BaseCompiler::sniffConditionalControlCmp(Cond compareOp,
ValType operandType) {
MOZ_ASSERT(latentOp_ == LatentOp::None,
"Latent comparison state not properly reset");
#ifdef JS_CODEGEN_X86
// On x86, latent i64 binary comparisons use too many registers: the
// reserved join register and the lhs and rhs operands require six, but we
// only have five.
if (operandType == ValType::I64) {
return false;
}
#endif
// No optimization for pointer compares yet.
if (operandType.isRefRepr()) {
return false;
}
OpBytes op{};
iter_.peekOp(&op);
switch (op.b0) {
case uint16_t(Op::BrIf):
case uint16_t(Op::If):
case uint16_t(Op::SelectNumeric):
case uint16_t(Op::SelectTyped):
setLatentCompare(compareOp, operandType);
return true;
default:
return false;
}
}
bool BaseCompiler::sniffConditionalControlEqz(ValType operandType) {
MOZ_ASSERT(latentOp_ == LatentOp::None,
"Latent comparison state not properly reset");
OpBytes op{};
iter_.peekOp(&op);
switch (op.b0) {
case uint16_t(Op::BrIf):
case uint16_t(Op::SelectNumeric):
case uint16_t(Op::SelectTyped):
case uint16_t(Op::If):
setLatentEqz(operandType);
return true;
default:
return false;
}
}
void BaseCompiler::emitBranchSetup(BranchState* b) {
// Avoid allocating operands to latentOp_ to result registers.
if (b->hasBlockResults()) {
needResultRegisters(b->resultType);
}
// Set up fields so that emitBranchPerform() need not switch on latentOp_.
switch (latentOp_) {
case LatentOp::None: {
latentIntCmp_ = Assembler::NotEqual;
latentType_ = ValType::I32;
b->i32.lhs = popI32();
b->i32.rhsImm = true;
b->i32.imm = 0;
break;
}
case LatentOp::Compare: {
switch (latentType_.kind()) {
case ValType::I32: {
if (popConst(&b->i32.imm)) {
b->i32.lhs = popI32();
b->i32.rhsImm = true;
} else {
pop2xI32(&b->i32.lhs, &b->i32.rhs);
b->i32.rhsImm = false;
}
break;
}
case ValType::I64: {
pop2xI64(&b->i64.lhs, &b->i64.rhs);
b->i64.rhsImm = false;
break;
}
case ValType::F32: {
pop2xF32(&b->f32.lhs, &b->f32.rhs);
break;
}
case ValType::F64: {
pop2xF64(&b->f64.lhs, &b->f64.rhs);
break;
}
default: {
MOZ_CRASH("Unexpected type for LatentOp::Compare");
}
}
break;
}
case LatentOp::Eqz: {
switch (latentType_.kind()) {
case ValType::I32: {
latentIntCmp_ = Assembler::Equal;
b->i32.lhs = popI32();
b->i32.rhsImm = true;
b->i32.imm = 0;
break;
}
case ValType::I64: {
latentIntCmp_ = Assembler::Equal;
b->i64.lhs = popI64();
b->i64.rhsImm = true;
b->i64.imm = 0;
break;
}
default: {
MOZ_CRASH("Unexpected type for LatentOp::Eqz");
}
}
break;
}
}
if (b->hasBlockResults()) {
freeResultRegisters(b->resultType);
}
}
bool BaseCompiler::emitBranchPerform(BranchState* b) {
switch (latentType_.kind()) {
case ValType::I32: {
if (b->i32.rhsImm) {
if (!jumpConditionalWithResults(b, latentIntCmp_, b->i32.lhs,
Imm32(b->i32.imm))) {
return false;
}
} else {
if (!jumpConditionalWithResults(b, latentIntCmp_, b->i32.lhs,
b->i32.rhs)) {
return false;
}
freeI32(b->i32.rhs);
}
freeI32(b->i32.lhs);
break;
}
case ValType::I64: {
if (b->i64.rhsImm) {
if (!jumpConditionalWithResults(b, latentIntCmp_, b->i64.lhs,
Imm64(b->i64.imm))) {
return false;
}
} else {
if (!jumpConditionalWithResults(b, latentIntCmp_, b->i64.lhs,
b->i64.rhs)) {
return false;
}
freeI64(b->i64.rhs);
}
freeI64(b->i64.lhs);
break;
}
case ValType::F32: {
if (!jumpConditionalWithResults(b, latentDoubleCmp_, b->f32.lhs,
b->f32.rhs)) {
return false;
}
freeF32(b->f32.lhs);
freeF32(b->f32.rhs);
break;
}
case ValType::F64: {
if (!jumpConditionalWithResults(b, latentDoubleCmp_, b->f64.lhs,
b->f64.rhs)) {
return false;
}
freeF64(b->f64.lhs);
freeF64(b->f64.rhs);
break;
}
default: {
MOZ_CRASH("Unexpected type for LatentOp::Compare");
}
}
resetLatentOp();
return true;
}
// For blocks and loops and ifs:
//
// - Sync the value stack before going into the block in order to simplify exit
// from the block: all exits from the block can assume that there are no
// live registers except the one carrying the exit value.
// - The block can accumulate a number of dead values on the stacks, so when
// branching out of the block or falling out at the end be sure to
// pop the appropriate stacks back to where they were on entry, while
// preserving the exit value.
// - A continue branch in a loop is much like an exit branch, but the branch
// value must not be preserved.
// - The exit value is always in a designated join register (type dependent).
bool BaseCompiler::emitBlock() {
BlockType type;
if (!iter_.readBlock(&type)) {
return false;
}
if (!deadCode_) {
sync(); // Simplifies branching out from block
}
initControl(controlItem(), type.params());
return true;
}
bool BaseCompiler::endBlock(ResultType type) {
Control& block = controlItem();
if (deadCode_) {
// Block does not fall through; reset stack.
fr.resetStackHeight(block.stackHeight, type);
popValueStackTo(block.stackSize);
} else {
// If the block label is used, we have a control join, so we need to shuffle
// fallthrough values into place. Otherwise if it's not a control join, we
// can leave the value stack alone.
MOZ_ASSERT(stk_.length() == block.stackSize + type.length());
if (block.label.used()) {
popBlockResults(type, block.stackHeight, ContinuationKind::Fallthrough);
}
block.bceSafeOnExit &= bceSafe_;
}
// Bind after cleanup: branches out will have popped the stack.
if (block.label.used()) {
masm.bind(&block.label);
if (deadCode_) {
captureResultRegisters(type);
deadCode_ = false;
}
if (!pushBlockResults(type)) {
return false;
}
}
bceSafe_ = block.bceSafeOnExit;
return true;
}
bool BaseCompiler::emitLoop() {
BlockType type;
if (!iter_.readLoop(&type)) {
return false;
}
if (!deadCode_) {
sync(); // Simplifies branching out from block
}
initControl(controlItem(), type.params());
bceSafe_ = 0;
if (!deadCode_) {
// Loop entry is a control join, so shuffle the entry parameters into the
// well-known locations.
if (!topBlockParams(type.params())) {
return false;
}
masm.nopAlign(CodeAlignment);
masm.bind(&controlItem(0).label);
// The interrupt check barfs if there are live registers.
sync();
if (!addInterruptCheck()) {
return false;
}
if (compilerEnv_.mode() == CompileMode::LazyTiering) {
// Create an unpatched hotness check and stash enough information that we
// can patch it with a value related to the loop's size when we get to
// the corresponding `end` opcode.
Maybe<CodeOffset> ctrDecOffset = addHotnessCheck();
if (ctrDecOffset.isNothing()) {
return false;
}
controlItem().loopBytecodeStart = iter_.lastOpcodeOffset();
controlItem().offsetOfCtrDec = ctrDecOffset.value();
}
}
return true;
}
// The bodies of the "then" and "else" arms can be arbitrary sequences
// of expressions, they push control and increment the nesting and can
// even be targeted by jumps. A branch to the "if" block branches to
// the exit of the if, ie, it's like "break". Consider:
//
// (func (result i32)
// (if (i32.const 1)
// (begin (br 1) (unreachable))
// (begin (unreachable)))
// (i32.const 1))
//
// The branch causes neither of the unreachable expressions to be
// evaluated.
bool BaseCompiler::emitIf() {
BlockType type;
Nothing unused_cond;
if (!iter_.readIf(&type, &unused_cond)) {
return false;
}
BranchState b(&controlItem().otherLabel, InvertBranch(true));
if (!deadCode_) {
needResultRegisters(type.params());
emitBranchSetup(&b);
freeResultRegisters(type.params());
sync();
} else {
resetLatentOp();
}
initControl(controlItem(), type.params());
if (!deadCode_) {
// Because params can flow immediately to results in the case of an empty
// "then" or "else" block, and the result of an if/then is a join in
// general, we shuffle params eagerly to the result allocations.
if (!topBlockParams(type.params())) {
return false;
}
if (!emitBranchPerform(&b)) {
return false;
}
}
return true;
}
bool BaseCompiler::endIfThen(ResultType type) {
Control& ifThen = controlItem();
// The parameters to the "if" logically flow to both the "then" and "else"
// blocks, but the "else" block is empty. Since we know that the "if"
// type-checks, that means that the "else" parameters are the "else" results,
// and that the "if"'s result type is the same as its parameter type.
if (deadCode_) {
// "then" arm does not fall through; reset stack.
fr.resetStackHeight(ifThen.stackHeight, type);
popValueStackTo(ifThen.stackSize);
if (!ifThen.deadOnArrival) {
captureResultRegisters(type);
}
} else {
MOZ_ASSERT(stk_.length() == ifThen.stackSize + type.length());
// Assume we have a control join, so place results in block result
// allocations.
popBlockResults(type, ifThen.stackHeight, ContinuationKind::Fallthrough);
MOZ_ASSERT(!ifThen.deadOnArrival);
}
if (ifThen.otherLabel.used()) {
masm.bind(&ifThen.otherLabel);
}
if (ifThen.label.used()) {
masm.bind(&ifThen.label);
}
if (!deadCode_) {
ifThen.bceSafeOnExit &= bceSafe_;
}
deadCode_ = ifThen.deadOnArrival;
if (!deadCode_) {
if (!pushBlockResults(type)) {
return false;
}
}
bceSafe_ = ifThen.bceSafeOnExit & ifThen.bceSafeOnEntry;
return true;
}
bool BaseCompiler::emitElse() {
ResultType params, results;
BaseNothingVector unused_thenValues{};
if (!iter_.readElse(&params, &results, &unused_thenValues)) {
return false;
}
Control& ifThenElse = controlItem(0);
// See comment in endIfThenElse, below.
// Exit the "then" branch.
ifThenElse.deadThenBranch = deadCode_;
if (deadCode_) {
fr.resetStackHeight(ifThenElse.stackHeight, results);
popValueStackTo(ifThenElse.stackSize);
} else {
MOZ_ASSERT(stk_.length() == ifThenElse.stackSize + results.length());
popBlockResults(results, ifThenElse.stackHeight, ContinuationKind::Jump);
freeResultRegisters(results);
MOZ_ASSERT(!ifThenElse.deadOnArrival);
}
if (!deadCode_) {
masm.jump(&ifThenElse.label);
}
if (ifThenElse.otherLabel.used()) {
masm.bind(&ifThenElse.otherLabel);
}
// Reset to the "else" branch.
if (!deadCode_) {
ifThenElse.bceSafeOnExit &= bceSafe_;
}
deadCode_ = ifThenElse.deadOnArrival;
bceSafe_ = ifThenElse.bceSafeOnEntry;
fr.resetStackHeight(ifThenElse.stackHeight, params);
if (!deadCode_) {
captureResultRegisters(params);
if (!pushBlockResults(params)) {
return false;
}
}
return true;
}
bool BaseCompiler::endIfThenElse(ResultType type) {
Control& ifThenElse = controlItem();
// The expression type is not a reliable guide to what we'll find
// on the stack, we could have (if E (i32.const 1) (unreachable))
// in which case the "else" arm is AnyType but the type of the
// full expression is I32. So restore whatever's there, not what
// we want to find there. The "then" arm has the same constraint.
if (deadCode_) {
// "then" arm does not fall through; reset stack.
fr.resetStackHeight(ifThenElse.stackHeight, type);
popValueStackTo(ifThenElse.stackSize);
} else {
MOZ_ASSERT(stk_.length() == ifThenElse.stackSize + type.length());
// Assume we have a control join, so place results in block result
// allocations.
popBlockResults(type, ifThenElse.stackHeight,
ContinuationKind::Fallthrough);
ifThenElse.bceSafeOnExit &= bceSafe_;
MOZ_ASSERT(!ifThenElse.deadOnArrival);
}
if (ifThenElse.label.used()) {
masm.bind(&ifThenElse.label);
}
bool joinLive =
!ifThenElse.deadOnArrival &&
(!ifThenElse.deadThenBranch || !deadCode_ || ifThenElse.label.bound());
if (joinLive) {
// No values were provided by the "then" path, but capture the values
// provided by the "else" path.
if (deadCode_) {
captureResultRegisters(type);
}
deadCode_ = false;
}
bceSafe_ = ifThenElse.bceSafeOnExit;
if (!deadCode_) {
if (!pushBlockResults(type)) {
return false;
}
}
return true;
}
bool BaseCompiler::emitEnd() {
LabelKind kind;
ResultType type;
BaseNothingVector unused_values{};
if (!iter_.readEnd(&kind, &type, &unused_values, &unused_values)) {
return false;
}
// Every label case is responsible to pop the control item at the appropriate
// time for the label case
switch (kind) {
case LabelKind::Body: {
if (!endBlock(type)) {
return false;
}
doReturn(ContinuationKind::Fallthrough);
iter_.popEnd();
MOZ_ASSERT(iter_.controlStackEmpty());
return iter_.endFunction(iter_.end());
}
case LabelKind::Block:
if (!endBlock(type)) {
return false;
}
iter_.popEnd();
break;
case LabelKind::Loop: {
if (compilerEnv_.mode() == CompileMode::LazyTiering) {
// These are set (or not set) together.
MOZ_ASSERT((controlItem().loopBytecodeStart != UINTPTR_MAX) ==
(controlItem().offsetOfCtrDec.bound()));
if (controlItem().loopBytecodeStart != UINTPTR_MAX) {
// If the above condition is false, the loop was in dead code and so
// there is no loop-head hotness check that needs to be patched. See
// ::emitLoop.
MOZ_ASSERT(controlItem().loopBytecodeStart <=
iter_.lastOpcodeOffset());
size_t loopBytecodeSize =
iter_.lastOpcodeOffset() - controlItem().loopBytecodeStart;
uint32_t step = BlockSizeToDownwardsStep(loopBytecodeSize);
// Don't try to patch the check if we've OOM'd, since the check might
// not actually exist.
if (masm.oom()) {
return false;
}
patchHotnessCheck(controlItem().offsetOfCtrDec, step);
}
}
// The end of a loop isn't a branch target, so we can just leave its
// results on the expression stack to be consumed by the outer block.
iter_.popEnd();
break;
}
case LabelKind::Then:
if (!endIfThen(type)) {
return false;
}
iter_.popEnd();
break;
case LabelKind::Else:
if (!endIfThenElse(type)) {
return false;
}
iter_.popEnd();
break;
case LabelKind::Try:
case LabelKind::Catch:
case LabelKind::CatchAll:
if (!endTryCatch(type)) {
return false;
}
iter_.popEnd();
break;
case LabelKind::TryTable:
if (!endTryTable(type)) {
return false;
}
iter_.popEnd();
break;
}
return true;
}
bool BaseCompiler::emitBr() {
uint32_t relativeDepth;
ResultType type;
BaseNothingVector unused_values{};
if (!iter_.readBr(&relativeDepth, &type, &unused_values)) {
return false;
}
if (deadCode_) {
return true;
}
Control& target = controlItem(relativeDepth);
target.bceSafeOnExit &= bceSafe_;
// Save any values in the designated join registers, as if the target block
// returned normally.
popBlockResults(type, target.stackHeight, ContinuationKind::Jump);
masm.jump(&target.label);
// The registers holding the join values are free for the remainder of this
// block.
freeResultRegisters(type);
deadCode_ = true;
return true;
}
bool BaseCompiler::emitBrIf() {
uint32_t relativeDepth;
ResultType type;
BaseNothingVector unused_values{};
Nothing unused_condition;
if (!iter_.readBrIf(&relativeDepth, &type, &unused_values,
&unused_condition)) {
return false;
}
if (deadCode_) {
resetLatentOp();
return true;
}
Control& target = controlItem(relativeDepth);
target.bceSafeOnExit &= bceSafe_;
BranchState b(&target.label, target.stackHeight, InvertBranch(false), type);
emitBranchSetup(&b);
return emitBranchPerform(&b);
}
bool BaseCompiler::emitBrOnNull() {
MOZ_ASSERT(!hasLatentOp());
uint32_t relativeDepth;
ResultType type;
BaseNothingVector unused_values{};
Nothing unused_condition;
if (!iter_.readBrOnNull(&relativeDepth, &type, &unused_values,
&unused_condition)) {
return false;
}
if (deadCode_) {
return true;
}
Control& target = controlItem(relativeDepth);
target.bceSafeOnExit &= bceSafe_;
BranchState b(&target.label, target.stackHeight, InvertBranch(false), type);
if (b.hasBlockResults()) {
needResultRegisters(b.resultType);
}
RegRef ref = popRef();
if (b.hasBlockResults()) {
freeResultRegisters(b.resultType);
}
if (!jumpConditionalWithResults(&b, Assembler::Equal, ref,
ImmWord(AnyRef::NullRefValue))) {
return false;
}
pushRef(ref);
return true;
}
bool BaseCompiler::emitBrOnNonNull() {
MOZ_ASSERT(!hasLatentOp());
uint32_t relativeDepth;
ResultType type;
BaseNothingVector unused_values{};
Nothing unused_condition;
if (!iter_.readBrOnNonNull(&relativeDepth, &type, &unused_values,
&unused_condition)) {
return false;
}
if (deadCode_) {
return true;
}
Control& target = controlItem(relativeDepth);
target.bceSafeOnExit &= bceSafe_;
BranchState b(&target.label, target.stackHeight, InvertBranch(false), type);
MOZ_ASSERT(b.hasBlockResults(), "br_on_non_null has block results");
// Don't allocate the result register used in the branch
needIntegerResultRegisters(b.resultType);
// Get the ref from the top of the stack
RegRef refCondition = popRef();
// Create a copy of the ref for passing to the on_non_null label,
// the original ref is used in the condition.
RegRef ref = needRef();
moveRef(refCondition, ref);
pushRef(ref);
freeIntegerResultRegisters(b.resultType);
if (!jumpConditionalWithResults(&b, Assembler::NotEqual, refCondition,
ImmWord(AnyRef::NullRefValue))) {
return false;
}
freeRef(refCondition);
// Dropping null reference.
dropValue();
return true;
}
bool BaseCompiler::emitBrTable() {
Uint32Vector depths;
uint32_t defaultDepth;
ResultType branchParams;
BaseNothingVector unused_values{};
Nothing unused_index;
// N.B., `branchParams' gets set to the type of the default branch target. In
// the presence of subtyping, it could be that the different branch targets
// have different types. Here we rely on the assumption that the value
// representations (e.g. Stk value types) of all branch target types are the
// same, in the baseline compiler. Notably, this means that all Ref types
// should be represented the same.
if (!iter_.readBrTable(&depths, &defaultDepth, &branchParams, &unused_values,
&unused_index)) {
return false;
}
if (deadCode_) {
return true;
}
// Don't use param registers for rc
needIntegerResultRegisters(branchParams);
// Table switch value always on top.
RegI32 rc = popI32();
freeIntegerResultRegisters(branchParams);
StackHeight resultsBase(0);
if (!topBranchParams(branchParams, &resultsBase)) {
return false;
}
Label dispatchCode;
masm.branch32(Assembler::Below, rc, Imm32(depths.length()), &dispatchCode);
// This is the out-of-range stub. rc is dead here but we don't need it.
shuffleStackResultsBeforeBranch(
resultsBase, controlItem(defaultDepth).stackHeight, branchParams);
controlItem(defaultDepth).bceSafeOnExit &= bceSafe_;
masm.jump(&controlItem(defaultDepth).label);
// Emit stubs. rc is dead in all of these but we don't need it.
//
// The labels in the vector are in the TempAllocator and will
// be freed by and by.
//
// TODO / OPTIMIZE (Bug 1316804): Branch directly to the case code if we
// can, don't emit an intermediate stub.
LabelVector stubs;
if (!stubs.reserve(depths.length())) {
return false;
}
for (uint32_t depth : depths) {
stubs.infallibleEmplaceBack(NonAssertingLabel());
masm.bind(&stubs.back());
shuffleStackResultsBeforeBranch(resultsBase, controlItem(depth).stackHeight,
branchParams);
controlItem(depth).bceSafeOnExit &= bceSafe_;
masm.jump(&controlItem(depth).label);
}
// Emit table.
Label theTable;
jumpTable(stubs, &theTable);
// Emit indirect jump. rc is live here.
tableSwitch(&theTable, rc, &dispatchCode);
deadCode_ = true;
// Clean up.
freeI32(rc);
popValueStackBy(branchParams.length());
return true;
}
bool BaseCompiler::emitTry() {
BlockType type;
if (!iter_.readTry(&type)) {
return false;
}
if (!deadCode_) {
// Simplifies jumping out, but it is also necessary so that control
// can re-enter the catch handler without restoring registers.
sync();
}
initControl(controlItem(), type.params());
if (!deadCode_) {
// Be conservative for BCE due to complex control flow in try blocks.
controlItem().bceSafeOnExit = 0;
if (!startTryNote(&controlItem().tryNoteIndex)) {
return false;
}
}
return true;
}
bool BaseCompiler::emitTryTable() {
BlockType type;
TryTableCatchVector catches;
if (!iter_.readTryTable(&type, &catches)) {
return false;
}
if (!deadCode_) {
// Simplifies jumping out, but it is also necessary so that control
// can re-enter the catch handler without restoring registers.
sync();
}
initControl(controlItem(), type.params());
// Be conservative for BCE due to complex control flow in try blocks.
controlItem().bceSafeOnExit = 0;
// Don't emit a landing pad if this whole try is dead code
if (deadCode_) {
return true;
}
// Emit a landing pad that exceptions will jump into. Jump over it for now.
Label skipLandingPad;
masm.jump(&skipLandingPad);
StackHeight prePadHeight = fr.stackHeight();
uint32_t padOffset = masm.currentOffset();
uint32_t padStackHeight = masm.framePushed();
// InstanceReg is live and contains this function's instance by the exception
// handling resume method. We keep it alive for use in loading the tag for
// each catch handler.
RegPtr instance = RegPtr(InstanceReg);
#ifndef RABALDR_PIN_INSTANCE
needPtr(instance);
#endif
// Load exception and tag from instance, clearing it in the process.
RegRef exn;
RegRef exnTag;
consumePendingException(instance, &exn, &exnTag);
// Get a register to hold the tags for each catch
RegRef catchTag = needRef();
bool hadCatchAll = false;
for (const TryTableCatch& tryTableCatch : catches) {
ResultType labelParams = ResultType::Vector(tryTableCatch.labelType);
Control& target = controlItem(tryTableCatch.labelRelativeDepth);
target.bceSafeOnExit = 0;
// Handle a catch_all by jumping to the target block
if (tryTableCatch.tagIndex == CatchAllIndex) {
// Capture the exnref if it has been requested, or else free it.
if (tryTableCatch.captureExnRef) {
pushRef(exn);
} else {
freeRef(exn);
}
// Free all of the other registers
freeRef(exnTag);
freeRef(catchTag);
#ifndef RABALDR_PIN_INSTANCE
freePtr(instance);
#endif
// Pop the results needed for the target branch and perform the jump
popBlockResults(labelParams, target.stackHeight, ContinuationKind::Jump);
masm.jump(&target.label);
freeResultRegisters(labelParams);
// Break from the loop and skip the implicit rethrow that's needed
// if we didn't have a catch_all
hadCatchAll = true;
break;
}
// This is a `catch $t`, load the tag type we're trying to match
const TagType& tagType = *codeMeta_.tags[tryTableCatch.tagIndex].type;
const TagOffsetVector& tagOffsets = tagType.argOffsets();
ResultType tagParams = tagType.resultType();
// Load the tag for this catch and compare it against the exception's tag.
// If they don't match, skip to the next catch handler.
Label skipCatch;
loadTag(instance, tryTableCatch.tagIndex, catchTag);
masm.branchPtr(Assembler::NotEqual, exnTag, catchTag, &skipCatch);
// The tags and instance are dead after we've had a match, free them
freeRef(exnTag);
freeRef(catchTag);
#ifndef RABALDR_PIN_INSTANCE
freePtr(instance);
#endif
// Allocate a register to hold the exception data pointer
RegPtr data = needPtr();
// Unpack the tag and jump to the block
masm.loadPtr(Address(exn, (int32_t)WasmExceptionObject::offsetOfData()),
data);
// This method can increase stk_.length() by an unbounded amount, so we need
// to perform an allocation here to accomodate the variable number of
// values. There is enough headroom for the fixed number of values. The
// general case is handled in emitBody.
if (!stk_.reserve(stk_.length() + labelParams.length())) {
return false;
}
for (uint32_t i = 0; i < tagParams.length(); i++) {
int32_t offset = tagOffsets[i];
switch (tagParams[i].kind()) {
case ValType::I32: {
RegI32 reg = needI32();
masm.load32(Address(data, offset), reg);
pushI32(reg);
break;
}
case ValType::I64: {
RegI64 reg = needI64();
masm.load64(Address(data, offset), reg);
pushI64(reg);
break;
}
case ValType::F32: {
RegF32 reg = needF32();
masm.loadFloat32(Address(data, offset), reg);
pushF32(reg);
break;
}
case ValType::F64: {
RegF64 reg = needF64();
masm.loadDouble(Address(data, offset), reg);
pushF64(reg);
break;
}
case ValType::V128: {
#ifdef ENABLE_WASM_SIMD
RegV128 reg = needV128();
masm.loadUnalignedSimd128(Address(data, offset), reg);
pushV128(reg);
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
case ValType::Ref: {
RegRef reg = needRef();
masm.loadPtr(Address(data, offset), reg);
pushRef(reg);
break;
}
}
}
// The exception data pointer is no longer live after unpacking the
// exception
freePtr(data);
// Capture the exnref if it has been requested, or else free it.
if (tryTableCatch.captureExnRef) {
pushRef(exn);
} else {
freeRef(exn);
}
// Pop the results needed for the target branch and perform the jump
popBlockResults(labelParams, target.stackHeight, ContinuationKind::Jump);
masm.jump(&target.label);
freeResultRegisters(labelParams);
// Reset the stack height for the skip to the next catch handler
fr.setStackHeight(prePadHeight);
masm.bind(&skipCatch);
// Reset ownership of the registers for the next catch handler we emit
needRef(exn);
needRef(exnTag);
needRef(catchTag);
#ifndef RABALDR_PIN_INSTANCE
needPtr(instance);
#endif
}
if (!hadCatchAll) {
// Free all registers, except for the exception
freeRef(exnTag);
freeRef(catchTag);
#ifndef RABALDR_PIN_INSTANCE
freePtr(instance);
#endif
// If none of the tag checks succeed and there is no catch_all,
// then we rethrow the exception
if (!throwFrom(exn)) {
return false;
}
} else {
// All registers should have been freed by the catch_all
MOZ_ASSERT(isAvailableRef(exn));
MOZ_ASSERT(isAvailableRef(exnTag));
MOZ_ASSERT(isAvailableRef(catchTag));
#ifndef RABALDR_PIN_INSTANCE
MOZ_ASSERT(isAvailablePtr(instance));
#endif
}
// Reset stack height for skipLandingPad, and bind it
fr.setStackHeight(prePadHeight);
masm.bind(&skipLandingPad);
// Start the try note for this try block, after the landing pad
if (!startTryNote(&controlItem().tryNoteIndex)) {
return false;
}
// Mark the try note to start at the landing pad we created above
TryNoteVector& tryNotes = masm.tryNotes();
TryNote& tryNote = tryNotes[controlItem().tryNoteIndex];
tryNote.setLandingPad(padOffset, padStackHeight);
return true;
}
void BaseCompiler::emitCatchSetup(LabelKind kind, Control& tryCatch,
const ResultType& resultType) {
// Catch ends the try or last catch, so we finish this like endIfThen.
if (deadCode_) {
fr.resetStackHeight(tryCatch.stackHeight, resultType);
popValueStackTo(tryCatch.stackSize);
} else {
// If the previous block is a catch, we need to handle the extra exception
// reference on the stack (for rethrow) and thus the stack size is 1 more.
MOZ_ASSERT(stk_.length() == tryCatch.stackSize + resultType.length() +
(kind == LabelKind::Try ? 0 : 1));
// Try jumps to the end of the try-catch block unless a throw is done.
if (kind == LabelKind::Try) {
popBlockResults(resultType, tryCatch.stackHeight, ContinuationKind::Jump);
} else {
popCatchResults(resultType, tryCatch.stackHeight);
}
MOZ_ASSERT(stk_.length() == tryCatch.stackSize);
freeResultRegisters(resultType);
MOZ_ASSERT(!tryCatch.deadOnArrival);
}
// Reset to this "catch" branch.
deadCode_ = tryCatch.deadOnArrival;
// We use the empty result type here because catch does *not* take the
// try-catch block parameters.
fr.resetStackHeight(tryCatch.stackHeight, ResultType::Empty());
if (deadCode_) {
return;
}
bceSafe_ = 0;
// The end of the previous try/catch jumps to the join point.
masm.jump(&tryCatch.label);
// Note end of try block for finding the catch block target. This needs
// to happen after the stack is reset to the correct height.
if (kind == LabelKind::Try) {
finishTryNote(controlItem().tryNoteIndex);
}
}
bool BaseCompiler::emitCatch() {
LabelKind kind;
uint32_t tagIndex;
ResultType paramType, resultType;
BaseNothingVector unused_tryValues{};
if (!iter_.readCatch(&kind, &tagIndex, &paramType, &resultType,
&unused_tryValues)) {
return false;
}
Control& tryCatch = controlItem();
emitCatchSetup(kind, tryCatch, resultType);
if (deadCode_) {
return true;
}
// Construct info used for the exception landing pad.
CatchInfo catchInfo(tagIndex);
if (!tryCatch.catchInfos.emplaceBack(catchInfo)) {
return false;
}
masm.bind(&tryCatch.catchInfos.back().label);
// Extract the arguments in the exception package and push them.
const SharedTagType& tagType = codeMeta_.tags[tagIndex].type;
const ValTypeVector& params = tagType->argTypes();
const TagOffsetVector& offsets = tagType->argOffsets();
// The landing pad uses the block return protocol to communicate the
// exception object pointer to the catch block.
ResultType exnResult = ResultType::Single(RefType::extern_());
captureResultRegisters(exnResult);
if (!pushBlockResults(exnResult)) {
return false;
}
RegRef exn = popRef();
RegPtr data = needPtr();
masm.loadPtr(Address(exn, (int32_t)WasmExceptionObject::offsetOfData()),
data);
// This method can increase stk_.length() by an unbounded amount, so we need
// to perform an allocation here to accomodate the variable number of values.
// There is enough headroom for the fixed number of values. The general case
// is handled in emitBody.
if (!stk_.reserve(stk_.length() + params.length() + 1)) {
return false;
}
// This reference is pushed onto the stack because a potential rethrow
// may need to access it. It is always popped at the end of the block.
pushRef(exn);
for (uint32_t i = 0; i < params.length(); i++) {
int32_t offset = offsets[i];
switch (params[i].kind()) {
case ValType::I32: {
RegI32 reg = needI32();
masm.load32(Address(data, offset), reg);
pushI32(reg);
break;
}
case ValType::I64: {
RegI64 reg = needI64();
masm.load64(Address(data, offset), reg);
pushI64(reg);
break;
}
case ValType::F32: {
RegF32 reg = needF32();
masm.loadFloat32(Address(data, offset), reg);
pushF32(reg);
break;
}
case ValType::F64: {
RegF64 reg = needF64();
masm.loadDouble(Address(data, offset), reg);
pushF64(reg);
break;
}
case ValType::V128: {
#ifdef ENABLE_WASM_SIMD
RegV128 reg = needV128();
masm.loadUnalignedSimd128(Address(data, offset), reg);
pushV128(reg);
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
case ValType::Ref: {
RegRef reg = needRef();
masm.loadPtr(Address(data, offset), reg);
pushRef(reg);
break;
}
}
}
freePtr(data);
return true;
}
bool BaseCompiler::emitCatchAll() {
LabelKind kind;
ResultType paramType, resultType;
BaseNothingVector unused_tryValues{};
if (!iter_.readCatchAll(&kind, &paramType, &resultType, &unused_tryValues)) {
return false;
}
Control& tryCatch = controlItem();
emitCatchSetup(kind, tryCatch, resultType);
if (deadCode_) {
return true;
}
CatchInfo catchInfo(CatchAllIndex);
if (!tryCatch.catchInfos.emplaceBack(catchInfo)) {
return false;
}
masm.bind(&tryCatch.catchInfos.back().label);
// The landing pad uses the block return protocol to communicate the
// exception object pointer to the catch block.
ResultType exnResult = ResultType::Single(RefType::extern_());
captureResultRegisters(exnResult);
// This reference is pushed onto the stack because a potential rethrow
// may need to access it. It is always popped at the end of the block.
return pushBlockResults(exnResult);
}
bool BaseCompiler::emitDelegate() {
uint32_t relativeDepth;
ResultType resultType;
BaseNothingVector unused_tryValues{};
if (!iter_.readDelegate(&relativeDepth, &resultType, &unused_tryValues)) {
return false;
}
if (!endBlock(resultType)) {
return false;
}
if (controlItem().deadOnArrival) {
return true;
}
// Mark the end of the try body. This may insert a nop.
finishTryNote(controlItem().tryNoteIndex);
// If the target block is a non-try block, skip over it and find the next
// try block or the very last block (to re-throw out of the function).
Control& lastBlock = controlOutermost();
while (controlKind(relativeDepth) != LabelKind::Try &&
controlKind(relativeDepth) != LabelKind::TryTable &&
&controlItem(relativeDepth) != &lastBlock) {
relativeDepth++;
}
Control& target = controlItem(relativeDepth);
TryNoteVector& tryNotes = masm.tryNotes();
TryNote& delegateTryNote = tryNotes[controlItem().tryNoteIndex];
if (&target == &lastBlock) {
// A delegate targeting the function body block means that any exception
// in this try needs to be propagated to the caller function. We use the
// delegate code offset of `0` as that will be in the prologue and cannot
// have a try note.
delegateTryNote.setDelegate(0);
} else {
// Delegate to one byte inside the beginning of the target try note, as
// that's when matches hit. Try notes are guaranteed to not be empty either
// and so this will not miss either.
const TryNote& targetTryNote = tryNotes[target.tryNoteIndex];
delegateTryNote.setDelegate(targetTryNote.tryBodyBegin() + 1);
}
return true;
}
bool BaseCompiler::endTryCatch(ResultType type) {
Control& tryCatch = controlItem();
LabelKind tryKind = controlKind(0);
if (deadCode_) {
fr.resetStackHeight(tryCatch.stackHeight, type);
popValueStackTo(tryCatch.stackSize);
} else {
// If the previous block is a catch, we must handle the extra exception
// reference on the stack (for rethrow) and thus the stack size is 1 more.
MOZ_ASSERT(stk_.length() == tryCatch.stackSize + type.length() +
(tryKind == LabelKind::Try ? 0 : 1));
// Assume we have a control join, so place results in block result
// allocations and also handle the implicit exception reference if needed.
if (tryKind == LabelKind::Try) {
popBlockResults(type, tryCatch.stackHeight, ContinuationKind::Jump);
} else {
popCatchResults(type, tryCatch.stackHeight);
}
MOZ_ASSERT(stk_.length() == tryCatch.stackSize);
// Since we will emit a landing pad after this and jump over it to get to
// the control join, we free these here and re-capture at the join.
freeResultRegisters(type);
masm.jump(&tryCatch.label);
MOZ_ASSERT(!tryCatch.bceSafeOnExit);
MOZ_ASSERT(!tryCatch.deadOnArrival);
}
deadCode_ = tryCatch.deadOnArrival;
if (deadCode_) {
return true;
}
// Create landing pad for all catch handlers in this block.
// When used for a catchless try block, this will generate a landing pad
// with no handlers and only the fall-back rethrow.
// The stack height also needs to be set not for a block result, but for the
// entry to the exception handlers. This is reset again below for the join.
StackHeight prePadHeight = fr.stackHeight();
fr.setStackHeight(tryCatch.stackHeight);
// If we are in a catchless try block, then there were no catch blocks to
// mark the end of the try note, so we need to end it here.
if (tryKind == LabelKind::Try) {
// Mark the end of the try body. This may insert a nop.
finishTryNote(controlItem().tryNoteIndex);
}
// The landing pad begins at this point
TryNoteVector& tryNotes = masm.tryNotes();
TryNote& tryNote = tryNotes[controlItem().tryNoteIndex];
tryNote.setLandingPad(masm.currentOffset(), masm.framePushed());
// Store the Instance that was left in InstanceReg by the exception
// handling mechanism, that is this frame's Instance but with the exception
// filled in Instance::pendingException.
fr.storeInstancePtr(InstanceReg);
// Load exception pointer from Instance and make sure that it is
// saved before the following call will clear it.
RegRef exn;
RegRef tag;
consumePendingException(RegPtr(InstanceReg), &exn, &tag);
// Get a register to hold the tags for each catch
RegRef catchTag = needRef();
// Ensure that the exception is assigned to the block return register
// before branching to a handler.
pushRef(exn);
ResultType exnResult = ResultType::Single(RefType::extern_());
popBlockResults(exnResult, tryCatch.stackHeight, ContinuationKind::Jump);
freeResultRegisters(exnResult);
bool hasCatchAll = false;
for (CatchInfo& info : tryCatch.catchInfos) {
if (info.tagIndex != CatchAllIndex) {
MOZ_ASSERT(!hasCatchAll);
loadTag(RegPtr(InstanceReg), info.tagIndex, catchTag);
masm.branchPtr(Assembler::Equal, tag, catchTag, &info.label);
} else {
masm.jump(&info.label);
hasCatchAll = true;
}
}
freeRef(catchTag);
freeRef(tag);
// If none of the tag checks succeed and there is no catch_all,
// then we rethrow the exception.
if (!hasCatchAll) {
captureResultRegisters(exnResult);
if (!pushBlockResults(exnResult) || !throwFrom(popRef())) {
return false;
}
}
// Reset stack height for join.
fr.setStackHeight(prePadHeight);
// Create join point.
if (tryCatch.label.used()) {
masm.bind(&tryCatch.label);
}
captureResultRegisters(type);
deadCode_ = tryCatch.deadOnArrival;
bceSafe_ = tryCatch.bceSafeOnExit;
return pushBlockResults(type);
}
bool BaseCompiler::endTryTable(ResultType type) {
if (!controlItem().deadOnArrival) {
// Mark the end of the try body. This may insert a nop.
finishTryNote(controlItem().tryNoteIndex);
}
return endBlock(type);
}
bool BaseCompiler::emitThrow() {
uint32_t tagIndex;
BaseNothingVector unused_argValues{};
if (!iter_.readThrow(&tagIndex, &unused_argValues)) {
return false;
}
if (deadCode_) {
return true;
}
const TagDesc& tagDesc = codeMeta_.tags[tagIndex];
const ResultType& params = tagDesc.type->resultType();
const TagOffsetVector& offsets = tagDesc.type->argOffsets();
// Load the tag object
#ifdef RABALDR_PIN_INSTANCE
RegPtr instance(InstanceReg);
#else
RegPtr instance = needPtr();
fr.loadInstancePtr(instance);
#endif
RegRef tag = needRef();
loadTag(instance, tagIndex, tag);
#ifndef RABALDR_PIN_INSTANCE
freePtr(instance);
#endif
// Create the new exception object that we will throw.
pushRef(tag);
if (!emitInstanceCall(SASigExceptionNew)) {
return false;
}
// Get registers for exn and data, excluding the prebarrier register
needPtr(RegPtr(PreBarrierReg));
RegRef exn = popRef();
RegPtr data = needPtr();
freePtr(RegPtr(PreBarrierReg));
masm.loadPtr(Address(exn, WasmExceptionObject::offsetOfData()), data);
for (int32_t i = params.length() - 1; i >= 0; i--) {
uint32_t offset = offsets[i];
switch (params[i].kind()) {
case ValType::I32: {
RegI32 reg = popI32();
masm.store32(reg, Address(data, offset));
freeI32(reg);
break;
}
case ValType::I64: {
RegI64 reg = popI64();
masm.store64(reg, Address(data, offset));
freeI64(reg);
break;
}
case ValType::F32: {
RegF32 reg = popF32();
masm.storeFloat32(reg, Address(data, offset));
freeF32(reg);
break;
}
case ValType::F64: {
RegF64 reg = popF64();
masm.storeDouble(reg, Address(data, offset));
freeF64(reg);
break;
}
case ValType::V128: {
#ifdef ENABLE_WASM_SIMD
RegV128 reg = popV128();
masm.storeUnalignedSimd128(reg, Address(data, offset));
freeV128(reg);
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
case ValType::Ref: {
RegPtr valueAddr(PreBarrierReg);
needPtr(valueAddr);
masm.computeEffectiveAddress(Address(data, offset), valueAddr);
RegRef rv = popRef();
pushPtr(data);
// emitBarrieredStore preserves exn, rv
if (!emitBarrieredStore(Some(exn), valueAddr, rv,
PreBarrierKind::Normal,
PostBarrierKind::Imprecise)) {
return false;
}
popPtr(data);
freeRef(rv);
break;
}
}
}
freePtr(data);
deadCode_ = true;
return throwFrom(exn);
}
bool BaseCompiler::emitThrowRef() {
Nothing unused{};
if (!iter_.readThrowRef(&unused)) {
return false;
}
if (deadCode_) {
return true;
}
RegRef exn = popRef();
Label ok;
masm.branchWasmAnyRefIsNull(false, exn, &ok);
trap(Trap::NullPointerDereference);
masm.bind(&ok);
deadCode_ = true;
return throwFrom(exn);
}
bool BaseCompiler::emitRethrow() {
uint32_t relativeDepth;
if (!iter_.readRethrow(&relativeDepth)) {
return false;
}
if (deadCode_) {
return true;
}
Control& tryCatch = controlItem(relativeDepth);
RegRef exn = needRef();
peekRefAt(tryCatch.stackSize, exn);
deadCode_ = true;
return throwFrom(exn);
}
bool BaseCompiler::emitDrop() {
if (!iter_.readDrop()) {
return false;
}
if (deadCode_) {
return true;
}
dropValue();
return true;
}
void BaseCompiler::doReturn(ContinuationKind kind) {
if (deadCode_) {
return;
}
StackHeight height = controlOutermost().stackHeight;
ResultType type = ResultType::Vector(funcType().results());
popBlockResults(type, height, kind);
masm.jump(&returnLabel_);
freeResultRegisters(type);
}
bool BaseCompiler::emitReturn() {
BaseNothingVector unused_values{};
if (!iter_.readReturn(&unused_values)) {
return false;
}
if (deadCode_) {
return true;
}
doReturn(ContinuationKind::Jump);
deadCode_ = true;
return true;
}
// For now, always sync() at the beginning of the call to easily save live
// values.
//
// TODO / OPTIMIZE (Bug 1316806): We may be able to avoid a full sync(), since
// all we want is to save live registers that won't be saved by the callee or
// that we need for outgoing args - we don't need to sync the locals. We can
// just push the necessary registers, it'll be like a lightweight sync.
//
// Even some of the pushing may be unnecessary if the registers will be consumed
// by the call, because then what we want is parallel assignment to the argument
// registers or onto the stack for outgoing arguments. A sync() is just
// simpler.
bool BaseCompiler::emitCall() {
uint32_t funcIndex;
BaseNothingVector args_{};
if (!iter_.readCall(&funcIndex, &args_)) {
return false;
}
if (deadCode_) {
return true;
}
bool import = codeMeta_.funcIsImport(funcIndex);
if (import) {
BuiltinModuleFuncId knownFuncImport = codeMeta_.knownFuncImport(funcIndex);
if (knownFuncImport != BuiltinModuleFuncId::None) {
const BuiltinModuleFunc& builtinModuleFunc =
BuiltinModuleFuncs::getFromId(knownFuncImport);
if (builtinModuleFunc.usesMemory()) {
// The final parameter of an builtinModuleFunc is implicitly the heap
// base
pushHeapBase(0);
}
// Call the builtinModuleFunc
return emitInstanceCall(*builtinModuleFunc.sig());
}
}
sync();
const FuncType& funcType = codeMeta_.getFuncType(funcIndex);
uint32_t numArgs = funcType.args().length();
size_t stackArgBytes = stackConsumed(numArgs);
ResultType resultType(ResultType::Vector(funcType.results()));
StackResultsLoc results;
if (!pushStackResultsForWasmCall(resultType, RegPtr(ABINonArgReg0),
&results)) {
return false;
}
FunctionCall baselineCall(ABIKind::Wasm,
import ? RestoreState::All : RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), NormalCallResults(results), &baselineCall,
CalleeOnStack::False)) {
return false;
}
CodeOffset raOffset;
if (import) {
raOffset = callImport(codeMeta_.offsetOfFuncImportInstanceData(funcIndex),
baselineCall);
} else {
raOffset = callDefinition(funcIndex, baselineCall);
}
if (!createStackMap("emitCall", raOffset)) {
return false;
}
popStackResultsAfterWasmCall(results, stackArgBytes);
endCall(baselineCall, stackArgBytes);
popValueStackBy(numArgs);
captureCallResultRegisters(resultType);
return pushWasmCallResults(baselineCall, resultType, results);
}
bool BaseCompiler::emitReturnCall() {
uint32_t funcIndex;
BaseNothingVector args_{};
if (!iter_.readReturnCall(&funcIndex, &args_)) {
return false;
}
if (deadCode_) {
return true;
}
sync();
if (!insertDebugCollapseFrame()) {
return false;
}
const FuncType& funcType = codeMeta_.getFuncType(funcIndex);
bool import = codeMeta_.funcIsImport(funcIndex);
uint32_t numArgs = funcType.args().length();
FunctionCall baselineCall(ABIKind::Wasm,
import ? RestoreState::All : RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), TailCallResults(funcType), &baselineCall,
CalleeOnStack::False)) {
return false;
}
ReturnCallAdjustmentInfo retCallInfo =
BuildReturnCallAdjustmentInfo(this->funcType(), funcType);
if (import) {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::Import);
CalleeDesc callee =
CalleeDesc::import(codeMeta_.offsetOfFuncImportInstanceData(funcIndex));
masm.wasmReturnCallImport(desc, callee, retCallInfo);
} else {
CallSiteDesc desc(bytecodeOffset(), CallSiteKind::ReturnFunc);
masm.wasmReturnCall(desc, funcIndex, retCallInfo);
}
MOZ_ASSERT(stackMapGenerator_.framePushedExcludingOutboundCallArgs.isSome());
stackMapGenerator_.framePushedExcludingOutboundCallArgs.reset();
popValueStackBy(numArgs);
deadCode_ = true;
return true;
}
bool BaseCompiler::emitCallIndirect() {
uint32_t funcTypeIndex;
uint32_t tableIndex;
Nothing callee_;
BaseNothingVector args_{};
if (!iter_.readCallIndirect(&funcTypeIndex, &tableIndex, &callee_, &args_)) {
return false;
}
if (deadCode_) {
return true;
}
// Stack: ... arg1 .. argn callee
replaceTableAddressWithClampedInt32(
codeMeta_.tables[tableIndex].addressType());
sync();
const FuncType& funcType = (*codeMeta_.types)[funcTypeIndex].funcType();
uint32_t numArgs = funcType.args().length() + 1;
size_t stackArgBytes = stackConsumed(numArgs);
ResultType resultType(ResultType::Vector(funcType.results()));
StackResultsLoc results;
if (!pushStackResultsForWasmCall(resultType, RegPtr(ABINonArgReg0),
&results)) {
return false;
}
// State and realm are restored as needed by by callIndirect (really by
// MacroAssembler::wasmCallIndirect).
FunctionCall baselineCall(ABIKind::Wasm, RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), NormalCallResults(results), &baselineCall,
CalleeOnStack::True)) {
return false;
}
const Stk& callee = peek(results.count());
CodeOffset fastCallOffset;
CodeOffset slowCallOffset;
if (!callIndirect(funcTypeIndex, tableIndex, callee, baselineCall,
/*tailCall*/ false, &fastCallOffset, &slowCallOffset)) {
return false;
}
if (!createStackMap("emitCallIndirect", fastCallOffset)) {
return false;
}
if (!createStackMap("emitCallIndirect", slowCallOffset)) {
return false;
}
popStackResultsAfterWasmCall(results, stackArgBytes);
endCall(baselineCall, stackArgBytes);
popValueStackBy(numArgs);
captureCallResultRegisters(resultType);
return pushWasmCallResults(baselineCall, resultType, results);
}
bool BaseCompiler::emitReturnCallIndirect() {
uint32_t funcTypeIndex;
uint32_t tableIndex;
Nothing callee_;
BaseNothingVector args_{};
if (!iter_.readReturnCallIndirect(&funcTypeIndex, &tableIndex, &callee_,
&args_)) {
return false;
}
if (deadCode_) {
return true;
}
// Stack: ... arg1 .. argn callee
replaceTableAddressWithClampedInt32(
codeMeta_.tables[tableIndex].addressType());
sync();
if (!insertDebugCollapseFrame()) {
return false;
}
const FuncType& funcType = (*codeMeta_.types)[funcTypeIndex].funcType();
uint32_t numArgs = funcType.args().length() + 1;
// State and realm are restored as needed by by callIndirect (really by
// MacroAssembler::wasmCallIndirect).
FunctionCall baselineCall(ABIKind::Wasm, RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), TailCallResults(funcType), &baselineCall,
CalleeOnStack::True)) {
return false;
}
const Stk& callee = peek(0);
CodeOffset fastCallOffset;
CodeOffset slowCallOffset;
if (!callIndirect(funcTypeIndex, tableIndex, callee, baselineCall,
/*tailCall*/ true, &fastCallOffset, &slowCallOffset)) {
return false;
}
MOZ_ASSERT(stackMapGenerator_.framePushedExcludingOutboundCallArgs.isSome());
stackMapGenerator_.framePushedExcludingOutboundCallArgs.reset();
popValueStackBy(numArgs);
deadCode_ = true;
return true;
}
bool BaseCompiler::emitCallRef() {
uint32_t funcTypeIndex;
Nothing unused_callee;
BaseNothingVector unused_args{};
if (!iter_.readCallRef(&funcTypeIndex, &unused_callee, &unused_args)) {
return false;
}
// Add a metrics entry to track this call_ref site. Do this even if we're in
// 'dead code' to have easy consistency with ion, which consumes these.
Maybe<size_t> callRefIndex;
if (compilerEnv_.mode() == CompileMode::LazyTiering) {
masm.append(wasm::CallRefMetricsPatch());
if (masm.oom()) {
return false;
}
callRefIndex = Some(masm.callRefMetricsPatches().length() - 1);
}
if (deadCode_) {
return true;
}
const FuncType& funcType = codeMeta_.types->type(funcTypeIndex).funcType();
sync();
// Stack: ... arg1 .. argn callee
uint32_t numArgs = funcType.args().length() + 1;
size_t stackArgBytes = stackConsumed(numArgs);
ResultType resultType(ResultType::Vector(funcType.results()));
StackResultsLoc results;
if (!pushStackResultsForWasmCall(resultType, RegPtr(ABINonArgReg0),
&results)) {
return false;
}
// State and realm are restored as needed by by callRef (really by
// MacroAssembler::wasmCallRef).
FunctionCall baselineCall(ABIKind::Wasm, RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), NormalCallResults(results), &baselineCall,
CalleeOnStack::True)) {
return false;
}
const Stk& callee = peek(results.count());
CodeOffset fastCallOffset;
CodeOffset slowCallOffset;
if (!callRef(callee, baselineCall, callRefIndex, &fastCallOffset,
&slowCallOffset)) {
return false;
}
if (!createStackMap("emitCallRef", fastCallOffset)) {
return false;
}
if (!createStackMap("emitCallRef", slowCallOffset)) {
return false;
}
popStackResultsAfterWasmCall(results, stackArgBytes);
endCall(baselineCall, stackArgBytes);
popValueStackBy(numArgs);
captureCallResultRegisters(resultType);
return pushWasmCallResults(baselineCall, resultType, results);
}
bool BaseCompiler::emitReturnCallRef() {
uint32_t funcTypeIndex;
Nothing unused_callee;
BaseNothingVector unused_args{};
if (!iter_.readReturnCallRef(&funcTypeIndex, &unused_callee, &unused_args)) {
return false;
}
if (deadCode_) {
return true;
}
const FuncType& funcType = codeMeta_.types->type(funcTypeIndex).funcType();
sync();
if (!insertDebugCollapseFrame()) {
return false;
}
// Stack: ... arg1 .. argn callee
uint32_t numArgs = funcType.args().length() + 1;
// State and realm are restored as needed by by callRef (really by
// MacroAssembler::wasmCallRef).
FunctionCall baselineCall(ABIKind::Wasm, RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(funcType.args(), TailCallResults(funcType), &baselineCall,
CalleeOnStack::True)) {
return false;
}
const Stk& callee = peek(0);
returnCallRef(callee, baselineCall, funcType);
MOZ_ASSERT(stackMapGenerator_.framePushedExcludingOutboundCallArgs.isSome());
stackMapGenerator_.framePushedExcludingOutboundCallArgs.reset();
popValueStackBy(numArgs);
deadCode_ = true;
return true;
}
void BaseCompiler::emitRound(RoundingMode roundingMode, ValType operandType) {
if (operandType == ValType::F32) {
RegF32 f0 = popF32();
roundF32(roundingMode, f0);
pushF32(f0);
} else if (operandType == ValType::F64) {
RegF64 f0 = popF64();
roundF64(roundingMode, f0);
pushF64(f0);
} else {
MOZ_CRASH("unexpected type");
}
}
bool BaseCompiler::emitUnaryMathBuiltinCall(SymbolicAddress callee,
ValType operandType) {
Nothing operand_;
if (!iter_.readUnary(operandType, &operand_)) {
return false;
}
if (deadCode_) {
return true;
}
RoundingMode roundingMode;
if (IsRoundingFunction(callee, &roundingMode) &&
supportsRoundInstruction(roundingMode)) {
emitRound(roundingMode, operandType);
return true;
}
sync();
ValTypeVector& signature = operandType == ValType::F32 ? SigF_ : SigD_;
ValType retType = operandType;
uint32_t numArgs = signature.length();
size_t stackSpace = stackConsumed(numArgs);
FunctionCall baselineCall(ABIKind::System, RestoreState::None);
beginCall(baselineCall);
if (!emitCallArgs(signature, NoCallResults(), &baselineCall,
CalleeOnStack::False)) {
return false;
}
CodeOffset raOffset = builtinCall(callee, baselineCall);
if (!createStackMap("emitUnaryMathBuiltin[..]", raOffset)) {
return false;
}
endCall(baselineCall, stackSpace);
popValueStackBy(numArgs);
pushBuiltinCallResult(baselineCall, retType.toMIRType());
return true;
}
#ifdef RABALDR_INT_DIV_I64_CALLOUT
bool BaseCompiler::emitDivOrModI64BuiltinCall(SymbolicAddress callee,
ValType operandType) {
MOZ_ASSERT(operandType == ValType::I64);
MOZ_ASSERT(!deadCode_);
sync();
needI64(specific_.abiReturnRegI64);
RegI64 rhs = popI64();
RegI64 srcDest = popI64ToSpecific(specific_.abiReturnRegI64);
Label done;
checkDivideByZero(rhs);
if (callee == SymbolicAddress::DivI64) {
checkDivideSignedOverflow(rhs, srcDest, &done, ZeroOnOverflow(false));
} else if (callee == SymbolicAddress::ModI64) {
checkDivideSignedOverflow(rhs, srcDest, &done, ZeroOnOverflow(true));
}
masm.setupWasmABICall();
masm.passABIArg(srcDest.high);
masm.passABIArg(srcDest.low);
masm.passABIArg(rhs.high);
masm.passABIArg(rhs.low);
CodeOffset raOffset = masm.callWithABI(
bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset()));
if (!createStackMap("emitDivOrModI64Bui[..]", raOffset)) {
return false;
}
masm.bind(&done);
freeI64(rhs);
pushI64(srcDest);
return true;
}
#endif // RABALDR_INT_DIV_I64_CALLOUT
#ifdef RABALDR_I64_TO_FLOAT_CALLOUT
bool BaseCompiler::emitConvertInt64ToFloatingCallout(SymbolicAddress callee,
ValType operandType,
ValType resultType) {
sync();
RegI64 input = popI64();
FunctionCall call(ABIKind::Wasm, RestoreState::None);
masm.setupWasmABICall();
# ifdef JS_PUNBOX64
MOZ_CRASH("BaseCompiler platform hook: emitConvertInt64ToFloatingCallout");
# else
masm.passABIArg(input.high);
masm.passABIArg(input.low);
# endif
CodeOffset raOffset = masm.callWithABI(
bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset()),
resultType == ValType::F32 ? ABIType::Float32 : ABIType::Float64);
if (!createStackMap("emitConvertInt64To[..]", raOffset)) {
return false;
}
freeI64(input);
if (resultType == ValType::F32) {
pushF32(captureReturnedF32(call));
} else {
pushF64(captureReturnedF64(call));
}
return true;
}
#endif // RABALDR_I64_TO_FLOAT_CALLOUT
#ifdef RABALDR_FLOAT_TO_I64_CALLOUT
// `Callee` always takes a double, so a float32 input must be converted.
bool BaseCompiler::emitConvertFloatingToInt64Callout(SymbolicAddress callee,
ValType operandType,
ValType resultType) {
RegF64 doubleInput;
if (operandType == ValType::F32) {
doubleInput = needF64();
RegF32 input = popF32();
masm.convertFloat32ToDouble(input, doubleInput);
freeF32(input);
} else {
doubleInput = popF64();
}
// We may need the value after the call for the ool check.
RegF64 otherReg = needF64();
moveF64(doubleInput, otherReg);
pushF64(otherReg);
sync();
FunctionCall call(ABIKind::Wasm, RestoreState::None);
masm.setupWasmABICall();
masm.passABIArg(doubleInput, ABIType::Float64);
CodeOffset raOffset = masm.callWithABI(
bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset()));
if (!createStackMap("emitConvertFloatin[..]", raOffset)) {
return false;
}
freeF64(doubleInput);
RegI64 rv = captureReturnedI64();
RegF64 inputVal = popF64();
TruncFlags flags = 0;
if (callee == SymbolicAddress::TruncateDoubleToUint64) {
flags |= TRUNC_UNSIGNED;
}
if (callee == SymbolicAddress::SaturatingTruncateDoubleToInt64 ||
callee == SymbolicAddress::SaturatingTruncateDoubleToUint64) {
flags |= TRUNC_SATURATING;
}
// If we're saturating, the callout will always produce the final result
// value. Otherwise, the callout value will return 0x8000000000000000
// and we need to produce traps.
OutOfLineCode* ool = nullptr;
if (!(flags & TRUNC_SATURATING)) {
// The OOL check just succeeds or fails, it does not generate a value.
ool = addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(
AnyReg(inputVal), rv, flags, trapSiteDesc()));
if (!ool) {
return false;
}
masm.branch64(Assembler::Equal, rv, Imm64(0x8000000000000000),
ool->entry());
masm.bind(ool->rejoin());
}
pushI64(rv);
freeF64(inputVal);
return true;
}
#endif // RABALDR_FLOAT_TO_I64_CALLOUT
bool BaseCompiler::emitGetLocal() {
uint32_t slot;
if (!iter_.readGetLocal(&slot)) {
return false;
}
if (deadCode_) {
return true;
}
// Local loads are pushed unresolved, ie, they may be deferred
// until needed, until they may be affected by a store, or until a
// sync. This is intended to reduce register pressure.
switch (locals_[slot].kind()) {
case ValType::I32:
pushLocalI32(slot);
break;
case ValType::I64:
pushLocalI64(slot);
break;
case ValType::V128:
#ifdef ENABLE_WASM_SIMD
pushLocalV128(slot);
break;
#else
MOZ_CRASH("No SIMD support");
#endif
case ValType::F64:
pushLocalF64(slot);
break;
case ValType::F32:
pushLocalF32(slot);
break;
case ValType::Ref:
pushLocalRef(slot);
break;
}
return true;
}
template <bool isSetLocal>
bool BaseCompiler::emitSetOrTeeLocal(uint32_t slot) {
if (deadCode_) {
return true;
}
bceLocalIsUpdated(slot);
switch (locals_[slot].kind()) {
case ValType::I32: {
RegI32 rv = popI32();
syncLocal(slot);
fr.storeLocalI32(rv, localFromSlot(slot, MIRType::Int32));
if (isSetLocal) {
freeI32(rv);
} else {
pushI32(rv);
}
break;
}
case ValType::I64: {
RegI64 rv = popI64();
syncLocal(slot);
fr.storeLocalI64(rv, localFromSlot(slot, MIRType::Int64));
if (isSetLocal) {
freeI64(rv);
} else {
pushI64(rv);
}
break;
}
case ValType::F64: {
RegF64 rv = popF64();
syncLocal(slot);
fr.storeLocalF64(rv, localFromSlot(slot, MIRType::Double));
if (isSetLocal) {
freeF64(rv);
} else {
pushF64(rv);
}
break;
}
case ValType::F32: {
RegF32 rv = popF32();
syncLocal(slot);
fr.storeLocalF32(rv, localFromSlot(slot, MIRType::Float32));
if (isSetLocal) {
freeF32(rv);
} else {
pushF32(rv);
}
break;
}
case ValType::V128: {
#ifdef ENABLE_WASM_SIMD
RegV128 rv = popV128();
syncLocal(slot);
fr.storeLocalV128(rv, localFromSlot(slot, MIRType::Simd128));
if (isSetLocal) {
freeV128(rv);
} else {
pushV128(rv);
}
break;
#else
MOZ_CRASH("No SIMD support");
#endif
}
case ValType::Ref: {
RegRef rv = popRef();
syncLocal(slot);
fr.storeLocalRef(rv, localFromSlot(slot, MIRType::WasmAnyRef));
if (isSetLocal) {
freeRef(rv);
} else {
pushRef(rv);
}
break;
}
}
return true;
}
bool BaseCompiler::emitSetLocal() {
uint32_t slot;
Nothing unused_value;
if (!iter_.readSetLocal(&slot, &unused_value)) {
return false;
}
return emitSetOrTeeLocal<true>(slot);
}
bool BaseCompiler::emitTeeLocal() {
uint32_t slot;
Nothing unused_value;
if (!iter_.readTeeLocal(&slot, &unused_value)) {
return false;
}
return emitSetOrTeeLocal<false>(slot);
}
bool BaseCompiler::emitGetGlobal() {
uint32_t id;
if (!iter_.readGetGlobal(&id)) {
return false;
}
if (deadCode_) {
return true;
}
const GlobalDesc& global = codeMeta_.globals[id];
if (global.isConstant()) {
LitVal value = global.constantValue();
switch (value.type().kind()) {
case ValType::I32:
pushI32(value.i32());
break;
case ValType::I64:
pushI64(value.i64());
break;
case ValType::F32:
pushF32(value.f32());
break;
case ValType::F64:
pushF64(value.f64());
break;
case ValType::Ref:
pushRef(intptr_t(value.ref().forCompiledCode()));
break;
#ifdef ENABLE_WASM_SIMD
case ValType::V128:
pushV128(value.v128());
break;
#endif
default:
MOZ_CRASH("Global constant type");
}
return true;
}
switch (global.type().kind()) {
case ValType::I32: {
RegI32 rv = needI32();
ScratchPtr tmp(*this);
masm.load32(addressOfGlobalVar(global, tmp), rv);
pushI32(rv);
break;
}
case ValType::I64: {
RegI64 rv = needI64();
ScratchPtr tmp(*this);
masm.load64(addressOfGlobalVar(global, tmp), rv);
pushI64(rv);
break;
}
case ValType::F32: {
RegF32 rv = needF32();
ScratchPtr tmp(*this);
masm.loadFloat32(addressOfGlobalVar(global, tmp), rv);
pushF32(rv);
break;
}
case ValType::F64: {
RegF64 rv = needF64();
ScratchPtr tmp(*this);
masm.loadDouble(addressOfGlobalVar(global, tmp), rv);
pushF64(rv);
break;
}
case ValType::Ref: {
RegRef rv = needRef();
ScratchPtr tmp(*this);
masm.loadPtr(addressOfGlobalVar(global, tmp), rv);
pushRef(rv);
break;
}
#ifdef ENABLE_WASM_SIMD
case ValType::V128: {
RegV128 rv = needV128();
ScratchPtr tmp(*this);
masm.loadUnalignedSimd128(addressOfGlobalVar(global, tmp), rv);
pushV128(rv);
break;
}
#endif
default:
MOZ_CRASH("Global variable type");
break;
}
return true;
}
bool BaseCompiler::emitSetGlobal() {
uint32_t id;
Nothing unused_value;
if (!iter_.readSetGlobal(&id, &unused_value)) {
return false;
}
if (deadCode_) {
return true;
}
const GlobalDesc& global = codeMeta_.globals[id];
switch (global.type().kind()) {
case ValType::I32: {
RegI32 rv = popI32();
ScratchPtr tmp(*this);
masm.store32(rv, addressOfGlobalVar(global, tmp));
freeI32(rv);
break;
}
case ValType::I64: {
RegI64 rv = popI64();
ScratchPtr tmp(*this);
masm.store64(rv, addressOfGlobalVar(global, tmp));
freeI64(rv);
break;
}
case ValType::F32: {
RegF32 rv = popF32();
ScratchPtr tmp(*this);
masm.storeFloat32(rv, addressOfGlobalVar(global, tmp));
freeF32(rv);
break;
}
case ValType::F64: {
RegF64 rv = popF64();
ScratchPtr tmp(*this);
masm.storeDouble(rv, addressOfGlobalVar(global, tmp));
freeF64(rv);
break;
}
case ValType::Ref: {
RegPtr valueAddr(PreBarrierReg);
needPtr(valueAddr);
{
ScratchPtr tmp(*this);
masm.computeEffectiveAddress(addressOfGlobalVar(global, tmp),
valueAddr);
}
RegRef rv = popRef();
// emitBarrieredStore preserves rv
if (!emitBarrieredStore(Nothing(), valueAddr, rv, PreBarrierKind::Normal,
PostBarrierKind::Imprecise)) {
return false;
}
freeRef(rv);
break;
}
#ifdef ENABLE_WASM_SIMD
case ValType::V128: {
RegV128 rv = popV128();
ScratchPtr tmp(*this);
masm.storeUnalignedSimd128(rv, addressOfGlobalVar(global, tmp));
freeV128(rv);
break;
}
#endif
default:
MOZ_CRASH("Global variable type");
break;
}
return true;
}
bool BaseCompiler::emitLoad(ValType type, Scalar::Type viewType) {
LinearMemoryAddress<Nothing> addr;
if (!iter_.readLoad(type, Scalar::byteSize(viewType), &addr)) {
return false;
}
if (deadCode_) {
return true;
}
MemoryAccessDesc access(addr.memoryIndex, viewType, addr.align, addr.offset,
trapSiteDesc(), hugeMemoryEnabled(addr.memoryIndex));
loadCommon(&access, AccessCheck(), type);
return true;
}
bool BaseCompiler::emitStore(ValType resultType, Scalar::Type viewType) {
LinearMemoryAddress<Nothing> addr;
Nothing unused_value;
if (!iter_.readStore(resultType, Scalar::byteSize(viewType), &addr,
&unused_value)) {
return false;
}
if (deadCode_) {
return true;
}
MemoryAccessDesc access(addr.memoryIndex, viewType, addr.align, addr.offset,
trapSiteDesc(), hugeMemoryEnabled(addr.memoryIndex));
storeCommon(&access, AccessCheck(), resultType);
return true;
}
bool BaseCompiler::emitSelect(bool typed) {
StackType type;
Nothing unused_trueValue;
Nothing unused_falseValue;
Nothing unused_condition;
if (!iter_.readSelect(typed, &type, &unused_trueValue, &unused_falseValue,
&unused_condition)) {
return false;
}
if (deadCode_) {
resetLatentOp();
return true;
}
// I32 condition on top, then false, then true.
Label done;
BranchState b(&done);
emitBranchSetup(&b);
switch (type.valType().kind()) {
case ValType::I32: {
RegI32 r, rs;
pop2xI32(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveI32(rs, r);
masm.bind(&done);
freeI32(rs);
pushI32(r);
break;
}
case ValType::I64: {
#ifdef JS_CODEGEN_X86
// There may be as many as four Int64 values in registers at a time: two
// for the latent branch operands, and two for the true/false values we
// normally pop before executing the branch. On x86 this is one value
// too many, so we need to generate more complicated code here, and for
// simplicity's sake we do so even if the branch operands are not Int64.
// However, the resulting control flow diamond is complicated since the
// arms of the diamond will have to stay synchronized with respect to
// their evaluation stack and regalloc state. To simplify further, we
// use a double branch and a temporary boolean value for now.
RegI32 temp = needI32();
moveImm32(0, temp);
if (!emitBranchPerform(&b)) {
return false;
}
moveImm32(1, temp);
masm.bind(&done);
Label trueValue;
RegI64 r, rs;
pop2xI64(&r, &rs);
masm.branch32(Assembler::Equal, temp, Imm32(0), &trueValue);
moveI64(rs, r);
masm.bind(&trueValue);
freeI32(temp);
freeI64(rs);
pushI64(r);
#else
RegI64 r, rs;
pop2xI64(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveI64(rs, r);
masm.bind(&done);
freeI64(rs);
pushI64(r);
#endif
break;
}
case ValType::F32: {
RegF32 r, rs;
pop2xF32(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveF32(rs, r);
masm.bind(&done);
freeF32(rs);
pushF32(r);
break;
}
case ValType::F64: {
RegF64 r, rs;
pop2xF64(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveF64(rs, r);
masm.bind(&done);
freeF64(rs);
pushF64(r);
break;
}
#ifdef ENABLE_WASM_SIMD
case ValType::V128: {
RegV128 r, rs;
pop2xV128(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveV128(rs, r);
masm.bind(&done);
freeV128(rs);
pushV128(r);
break;
}
#endif
case ValType::Ref: {
RegRef r, rs;
pop2xRef(&r, &rs);
if (!emitBranchPerform(&b)) {
return false;
}
moveRef(rs, r);
masm.bind(&done);
freeRef(rs);
pushRef(r);
break;
}
default: {
MOZ_CRASH("select type");
}
}
return true;
}
void BaseCompiler::emitCompareI32(Assembler::Condition compareOp,
ValType compareType) {
MOZ_ASSERT(compareType == ValType::I32);
if (sniffConditionalControlCmp(compareOp, compareType)) {
return;
}
int32_t c;
if (popConst(&c)) {
RegI32 r = popI32();
masm.cmp32Set(compareOp, r, Imm32(c), r);
pushI32(r);
} else {
RegI32 r, rs;
pop2xI32(&r, &rs);
masm.cmp32Set(compareOp, r, rs, r);
freeI32(rs);
pushI32(r);
}
}
void BaseCompiler::emitCompareI64(Assembler::Condition compareOp,
ValType compareType) {
MOZ_ASSERT(compareType == ValType::I64);
if (sniffConditionalControlCmp(compareOp, compareType)) {
return;
}
RegI64 rs0, rs1;
pop2xI64(&rs0, &rs1);
RegI32 rd(fromI64(rs0));
cmp64Set(compareOp, rs0, rs1, rd);
freeI64(rs1);
freeI64Except(rs0, rd);
pushI32(rd);
}
void BaseCompiler::emitCompareF32(Assembler::DoubleCondition compareOp,
ValType compareType) {
MOZ_ASSERT(compareType == ValType::F32);
if (sniffConditionalControlCmp(compareOp, compareType)) {
return;
}
Label across;
RegF32 rs0, rs1;
pop2xF32(&rs0, &rs1);
RegI32 rd = needI32();
moveImm32(1, rd);
masm.branchFloat(compareOp, rs0, rs1, &across);
moveImm32(0, rd);
masm.bind(&across);
freeF32(rs0);
freeF32(rs1);
pushI32(rd);
}
void BaseCompiler::emitCompareF64(Assembler::DoubleCondition compareOp,
ValType compareType) {