blob: 48437635831f1382beba708b67f039d83edf3ecb [file] [log] [blame] [edit]
/*
* Copyright 2017 WebAssembly Community Group participants
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
#include <mutex>
#include <set>
#include <sstream>
#include <unordered_set>
#include "ir/branch-utils.h"
#include "ir/features.h"
#include "ir/global-utils.h"
#include "ir/module-utils.h"
#include "ir/utils.h"
#include "support/colors.h"
#include "wasm-printing.h"
#include "wasm-validator.h"
#include "wasm.h"
namespace wasm {
// Print anything that can be streamed to an ostream
template<typename T,
typename std::enable_if<!std::is_base_of<
Expression,
typename std::remove_pointer<T>::type>::value>::type* = nullptr>
inline std::ostream& printModuleComponent(T curr, std::ostream& stream) {
stream << curr << std::endl;
return stream;
}
// Extra overload for Expressions, to print type info too
inline std::ostream& printModuleComponent(Expression* curr,
std::ostream& stream) {
WasmPrinter::printExpression(curr, stream, false, true) << std::endl;
return stream;
}
// For parallel validation, we have a helper struct for coordination
struct ValidationInfo {
bool validateWeb;
bool validateGlobally;
bool quiet;
std::atomic<bool> valid;
// a stream of error test for each function. we print in the right order at
// the end, for deterministic output
// note errors are rare/unexpected, so it's ok to use a slow mutex here
std::mutex mutex;
std::unordered_map<Function*, std::unique_ptr<std::ostringstream>> outputs;
ValidationInfo() { valid.store(true); }
std::ostringstream& getStream(Function* func) {
std::unique_lock<std::mutex> lock(mutex);
auto iter = outputs.find(func);
if (iter != outputs.end()) {
return *(iter->second.get());
}
auto& ret = outputs[func] = make_unique<std::ostringstream>();
return *ret.get();
}
// printing and error handling support
template<typename T, typename S>
std::ostream& fail(S text, T curr, Function* func) {
valid.store(false);
auto& stream = getStream(func);
if (quiet) {
return stream;
}
auto& ret = printFailureHeader(func);
ret << text << ", on \n";
return printModuleComponent(curr, ret);
}
std::ostream& printFailureHeader(Function* func) {
auto& stream = getStream(func);
if (quiet) {
return stream;
}
Colors::red(stream);
if (func) {
stream << "[wasm-validator error in function ";
Colors::green(stream);
stream << func->name;
Colors::red(stream);
stream << "] ";
} else {
stream << "[wasm-validator error in module] ";
}
Colors::normal(stream);
return stream;
}
// checking utilities
template<typename T>
bool shouldBeTrue(bool result,
T curr,
const char* text,
Function* func = nullptr) {
if (!result) {
fail("unexpected false: " + std::string(text), curr, func);
return false;
}
return result;
}
template<typename T>
bool shouldBeFalse(bool result,
T curr,
const char* text,
Function* func = nullptr) {
if (result) {
fail("unexpected true: " + std::string(text), curr, func);
return false;
}
return result;
}
template<typename T, typename S>
bool shouldBeEqual(
S left, S right, T curr, const char* text, Function* func = nullptr) {
if (left != right) {
std::ostringstream ss;
ss << left << " != " << right << ": " << text;
fail(ss.str(), curr, func);
return false;
}
return true;
}
template<typename T, typename S>
bool shouldBeEqualOrFirstIsUnreachable(
S left, S right, T curr, const char* text, Function* func = nullptr) {
if (left != Type::unreachable && left != right) {
std::ostringstream ss;
ss << left << " != " << right << ": " << text;
fail(ss.str(), curr, func);
return false;
}
return true;
}
template<typename T, typename S>
bool shouldBeUnequal(
S left, S right, T curr, const char* text, Function* func = nullptr) {
if (left == right) {
std::ostringstream ss;
ss << left << " == " << right << ": " << text;
fail(ss.str(), curr, func);
return false;
}
return true;
}
void shouldBeIntOrUnreachable(Type ty,
Expression* curr,
const char* text,
Function* func = nullptr) {
switch (ty.getSingle()) {
case Type::i32:
case Type::i64:
case Type::unreachable: {
break;
}
default:
fail(text, curr, func);
}
}
// Type 'left' should be a subtype of 'right'.
bool shouldBeSubType(Type left,
Type right,
Expression* curr,
const char* text,
Function* func = nullptr) {
if (Type::isSubType(left, right)) {
return true;
}
fail(text, curr, func);
return false;
}
// Type 'left' should be a subtype of 'right', or unreachable.
bool shouldBeSubTypeOrFirstIsUnreachable(Type left,
Type right,
Expression* curr,
const char* text,
Function* func = nullptr) {
if (left == Type::unreachable) {
return true;
}
return shouldBeSubType(left, right, curr, text, func);
}
};
struct FunctionValidator : public WalkerPass<PostWalker<FunctionValidator>> {
bool isFunctionParallel() override { return true; }
Pass* create() override { return new FunctionValidator(&info); }
bool modifiesBinaryenIR() override { return false; }
ValidationInfo& info;
FunctionValidator(ValidationInfo* info) : info(*info) {}
struct BreakInfo {
enum { UnsetArity = Index(-1), PoisonArity = Index(-2) };
Type type;
Index arity;
BreakInfo() : arity(UnsetArity) {}
BreakInfo(Type type, Index arity) : type(type), arity(arity) {}
bool hasBeenSet() {
// Compare to the impossible value.
return arity != UnsetArity;
}
};
std::unordered_map<Name, BreakInfo> breakInfos;
std::set<Type> returnTypes; // types used in returns
// Binaryen IR requires that label names must be unique - IR generators must
// ensure that
std::unordered_set<Name> labelNames;
void noteLabelName(Name name);
public:
// visitors
static void visitPreBlock(FunctionValidator* self, Expression** currp) {
auto* curr = (*currp)->cast<Block>();
if (curr->name.is()) {
self->breakInfos[curr->name];
}
}
void visitBlock(Block* curr);
static void visitPreLoop(FunctionValidator* self, Expression** currp) {
auto* curr = (*currp)->cast<Loop>();
if (curr->name.is()) {
self->breakInfos[curr->name];
}
}
void visitLoop(Loop* curr);
void visitIf(If* curr);
// override scan to add a pre and a post check task to all nodes
static void scan(FunctionValidator* self, Expression** currp) {
PostWalker<FunctionValidator>::scan(self, currp);
auto* curr = *currp;
if (curr->is<Block>()) {
self->pushTask(visitPreBlock, currp);
}
if (curr->is<Loop>()) {
self->pushTask(visitPreLoop, currp);
}
}
void noteBreak(Name name, Expression* value, Expression* curr);
void noteBreak(Name name, Type valueType, Expression* curr);
void visitBreak(Break* curr);
void visitSwitch(Switch* curr);
void visitCall(Call* curr);
void visitCallIndirect(CallIndirect* curr);
void visitConst(Const* curr);
void visitLocalGet(LocalGet* curr);
void visitLocalSet(LocalSet* curr);
void visitGlobalGet(GlobalGet* curr);
void visitGlobalSet(GlobalSet* curr);
void visitLoad(Load* curr);
void visitStore(Store* curr);
void visitAtomicRMW(AtomicRMW* curr);
void visitAtomicCmpxchg(AtomicCmpxchg* curr);
void visitAtomicWait(AtomicWait* curr);
void visitAtomicNotify(AtomicNotify* curr);
void visitAtomicFence(AtomicFence* curr);
void visitSIMDExtract(SIMDExtract* curr);
void visitSIMDReplace(SIMDReplace* curr);
void visitSIMDShuffle(SIMDShuffle* curr);
void visitSIMDTernary(SIMDTernary* curr);
void visitSIMDShift(SIMDShift* curr);
void visitSIMDLoad(SIMDLoad* curr);
void visitMemoryInit(MemoryInit* curr);
void visitDataDrop(DataDrop* curr);
void visitMemoryCopy(MemoryCopy* curr);
void visitMemoryFill(MemoryFill* curr);
void visitBinary(Binary* curr);
void visitUnary(Unary* curr);
void visitSelect(Select* curr);
void visitDrop(Drop* curr);
void visitReturn(Return* curr);
void visitHost(Host* curr);
void visitRefIsNull(RefIsNull* curr);
void visitRefFunc(RefFunc* curr);
void visitTry(Try* curr);
void visitThrow(Throw* curr);
void visitRethrow(Rethrow* curr);
void visitBrOnExn(BrOnExn* curr);
void visitFunction(Function* curr);
// helpers
private:
std::ostream& getStream() { return info.getStream(getFunction()); }
template<typename T>
bool shouldBeTrue(bool result, T curr, const char* text) {
return info.shouldBeTrue(result, curr, text, getFunction());
}
template<typename T>
bool shouldBeFalse(bool result, T curr, const char* text) {
return info.shouldBeFalse(result, curr, text, getFunction());
}
template<typename T, typename S>
bool shouldBeEqual(S left, S right, T curr, const char* text) {
return info.shouldBeEqual(left, right, curr, text, getFunction());
}
template<typename T, typename S>
bool
shouldBeEqualOrFirstIsUnreachable(S left, S right, T curr, const char* text) {
return info.shouldBeEqualOrFirstIsUnreachable(
left, right, curr, text, getFunction());
}
template<typename T, typename S>
bool shouldBeUnequal(S left, S right, T curr, const char* text) {
return info.shouldBeUnequal(left, right, curr, text, getFunction());
}
void shouldBeIntOrUnreachable(Type ty, Expression* curr, const char* text) {
return info.shouldBeIntOrUnreachable(ty, curr, text, getFunction());
}
bool
shouldBeSubType(Type left, Type right, Expression* curr, const char* text) {
return info.shouldBeSubType(left, right, curr, text, getFunction());
}
bool shouldBeSubTypeOrFirstIsUnreachable(Type left,
Type right,
Expression* curr,
const char* text) {
return info.shouldBeSubTypeOrFirstIsUnreachable(
left, right, curr, text, getFunction());
}
void validateAlignment(
size_t align, Type type, Index bytes, bool isAtomic, Expression* curr);
void validateMemBytes(uint8_t bytes, Type type, Expression* curr);
};
void FunctionValidator::noteLabelName(Name name) {
if (!name.is()) {
return;
}
bool inserted;
std::tie(std::ignore, inserted) = labelNames.insert(name);
shouldBeTrue(
inserted,
name,
"names in Binaryen IR must be unique - IR generators must ensure that");
}
void FunctionValidator::visitBlock(Block* curr) {
// if we are break'ed to, then the value must be right for us
if (curr->name.is()) {
noteLabelName(curr->name);
auto iter = breakInfos.find(curr->name);
assert(iter != breakInfos.end()); // we set it ourselves
auto& info = iter->second;
if (info.hasBeenSet()) {
if (curr->type.isConcrete()) {
shouldBeTrue(info.arity != 0,
curr,
"break arities must be > 0 if block has a value");
} else {
shouldBeTrue(info.arity == 0,
curr,
"break arities must be 0 if block has no value");
}
// none or unreachable means a poison value that we should ignore - if
// consumed, it will error
if (info.type.isConcrete() && curr->type.isConcrete()) {
shouldBeSubType(
info.type,
curr->type,
curr,
"block+breaks must have right type if breaks return a value");
}
if (curr->type.isConcrete() && info.arity &&
info.type != Type::unreachable) {
shouldBeSubType(
info.type,
curr->type,
curr,
"block+breaks must have right type if breaks have arity");
}
shouldBeTrue(
info.arity != BreakInfo::PoisonArity, curr, "break arities must match");
if (curr->list.size() > 0) {
auto last = curr->list.back()->type;
if (last == Type::none) {
shouldBeTrue(info.arity == Index(0),
curr,
"if block ends with a none, breaks cannot send a value "
"of any type");
}
}
}
breakInfos.erase(iter);
}
if (curr->list.size() > 1) {
for (Index i = 0; i < curr->list.size() - 1; i++) {
if (!shouldBeTrue(
!curr->list[i]->type.isConcrete(),
curr,
"non-final block elements returning a value must be drop()ed "
"(binaryen's autodrop option might help you)") &&
!info.quiet) {
getStream() << "(on index " << i << ":\n"
<< curr->list[i] << "\n), type: " << curr->list[i]->type
<< "\n";
}
}
}
if (curr->list.size() > 0) {
auto backType = curr->list.back()->type;
if (!curr->type.isConcrete()) {
shouldBeFalse(backType.isConcrete(),
curr,
"if block is not returning a value, final element should "
"not flow out a value");
} else {
if (backType.isConcrete()) {
shouldBeSubType(
backType,
curr->type,
curr,
"block with value and last element with value must match types");
} else {
shouldBeUnequal(
backType,
Type(Type::none),
curr,
"block with value must not have last element that is none");
}
}
}
if (curr->type.isConcrete()) {
shouldBeTrue(
curr->list.size() > 0, curr, "block with a value must not be empty");
}
}
void FunctionValidator::visitLoop(Loop* curr) {
if (curr->name.is()) {
noteLabelName(curr->name);
auto iter = breakInfos.find(curr->name);
assert(iter != breakInfos.end()); // we set it ourselves
auto& info = iter->second;
if (info.hasBeenSet()) {
shouldBeEqual(
info.arity, Index(0), curr, "breaks to a loop cannot pass a value");
}
breakInfos.erase(iter);
}
if (curr->type == Type::none) {
shouldBeFalse(curr->body->type.isConcrete(),
curr,
"bad body for a loop that has no value");
}
// When there are multiple instructions within a loop, they are wrapped in a
// Block internally, so visitBlock can take care of verification. Here we
// check cases when there is only one instruction in a Loop.
if (!curr->body->is<Block>()) {
if (!curr->type.isConcrete()) {
shouldBeFalse(curr->body->type.isConcrete(),
curr,
"if loop is not returning a value, final element should "
"not flow out a value");
} else {
shouldBeSubTypeOrFirstIsUnreachable(
curr->body->type,
curr->type,
curr,
"loop with value and body must match types");
}
}
}
void FunctionValidator::visitIf(If* curr) {
shouldBeTrue(curr->condition->type == Type::unreachable ||
curr->condition->type == Type::i32,
curr,
"if condition must be valid");
if (!curr->ifFalse) {
shouldBeFalse(curr->ifTrue->type.isConcrete(),
curr,
"if without else must not return a value in body");
if (curr->condition->type != Type::unreachable) {
shouldBeEqual(curr->type,
Type(Type::none),
curr,
"if without else and reachable condition must be none");
}
} else {
if (curr->type != Type::unreachable) {
shouldBeSubTypeOrFirstIsUnreachable(
curr->ifTrue->type,
curr->type,
curr,
"returning if-else's true must have right type");
shouldBeSubTypeOrFirstIsUnreachable(
curr->ifFalse->type,
curr->type,
curr,
"returning if-else's false must have right type");
} else {
if (curr->condition->type != Type::unreachable) {
shouldBeEqual(curr->ifTrue->type,
Type(Type::unreachable),
curr,
"unreachable if-else must have unreachable true");
shouldBeEqual(curr->ifFalse->type,
Type(Type::unreachable),
curr,
"unreachable if-else must have unreachable false");
}
}
if (curr->ifTrue->type.isConcrete()) {
shouldBeSubType(curr->ifTrue->type,
curr->type,
curr,
"if type must match concrete ifTrue");
}
if (curr->ifFalse->type.isConcrete()) {
shouldBeSubType(curr->ifFalse->type,
curr->type,
curr,
"if type must match concrete ifFalse");
}
}
}
void FunctionValidator::noteBreak(Name name,
Expression* value,
Expression* curr) {
if (value) {
shouldBeUnequal(
value->type, Type(Type::none), curr, "breaks must have a valid value");
}
noteBreak(name, value ? value->type : Type::none, curr);
}
void FunctionValidator::noteBreak(Name name, Type valueType, Expression* curr) {
Index arity = 0;
if (valueType != Type::none) {
arity = 1;
}
auto iter = breakInfos.find(name);
if (!shouldBeTrue(
iter != breakInfos.end(), curr, "all break targets must be valid")) {
return;
}
auto& info = iter->second;
if (!info.hasBeenSet()) {
info = BreakInfo(valueType, arity);
} else {
info.type = Type::getLeastUpperBound(info.type, valueType);
if (arity != info.arity) {
info.arity = BreakInfo::PoisonArity;
}
}
}
void FunctionValidator::visitBreak(Break* curr) {
noteBreak(curr->name, curr->value, curr);
if (curr->value) {
shouldBeTrue(curr->value->type != Type::none,
curr,
"break value must not have none type");
}
if (curr->condition) {
shouldBeTrue(curr->condition->type == Type::unreachable ||
curr->condition->type == Type::i32,
curr,
"break condition must be i32");
}
}
void FunctionValidator::visitSwitch(Switch* curr) {
for (auto& target : curr->targets) {
noteBreak(target, curr->value, curr);
}
noteBreak(curr->default_, curr->value, curr);
shouldBeTrue(curr->condition->type == Type::unreachable ||
curr->condition->type == Type::i32,
curr,
"br_table condition must be i32");
}
void FunctionValidator::visitCall(Call* curr) {
shouldBeTrue(!curr->isReturn || getModule()->features.hasTailCall(),
curr,
"return_call requires tail calls to be enabled");
if (!info.validateGlobally) {
return;
}
auto* target = getModule()->getFunctionOrNull(curr->target);
if (!shouldBeTrue(!!target, curr, "call target must exist")) {
return;
}
const std::vector<Type> params = target->sig.params.expand();
if (!shouldBeTrue(curr->operands.size() == params.size(),
curr,
"call param number must match")) {
return;
}
for (size_t i = 0; i < curr->operands.size(); i++) {
if (!shouldBeSubTypeOrFirstIsUnreachable(curr->operands[i]->type,
params[i],
curr,
"call param types must match") &&
!info.quiet) {
getStream() << "(on argument " << i << ")\n";
}
}
if (curr->isReturn) {
shouldBeEqual(curr->type,
Type(Type::unreachable),
curr,
"return_call should have unreachable type");
shouldBeEqual(
getFunction()->sig.results,
target->sig.results,
curr,
"return_call callee return type must match caller return type");
} else {
if (curr->type == Type::unreachable) {
bool hasUnreachableOperand = std::any_of(
curr->operands.begin(), curr->operands.end(), [](Expression* op) {
return op->type == Type::unreachable;
});
shouldBeTrue(
hasUnreachableOperand,
curr,
"calls may only be unreachable if they have unreachable operands");
} else {
shouldBeEqual(curr->type,
target->sig.results,
curr,
"call type must match callee return type");
}
}
}
void FunctionValidator::visitCallIndirect(CallIndirect* curr) {
shouldBeTrue(!curr->isReturn || getModule()->features.hasTailCall(),
curr,
"return_call_indirect requires tail calls to be enabled");
if (!info.validateGlobally) {
return;
}
const std::vector<Type>& params = curr->sig.params.expand();
shouldBeEqualOrFirstIsUnreachable(curr->target->type,
Type(Type::i32),
curr,
"indirect call target must be an i32");
if (!shouldBeTrue(curr->operands.size() == params.size(),
curr,
"call param number must match")) {
return;
}
for (size_t i = 0; i < curr->operands.size(); i++) {
if (!shouldBeSubTypeOrFirstIsUnreachable(curr->operands[i]->type,
params[i],
curr,
"call param types must match") &&
!info.quiet) {
getStream() << "(on argument " << i << ")\n";
}
}
if (curr->isReturn) {
shouldBeEqual(curr->type,
Type(Type::unreachable),
curr,
"return_call_indirect should have unreachable type");
shouldBeEqual(
getFunction()->sig.results,
curr->sig.results,
curr,
"return_call_indirect callee return type must match caller return type");
} else {
if (curr->type == Type::unreachable) {
if (curr->target->type != Type::unreachable) {
bool hasUnreachableOperand = std::any_of(
curr->operands.begin(), curr->operands.end(), [](Expression* op) {
return op->type == Type::unreachable;
});
shouldBeTrue(hasUnreachableOperand,
curr,
"call_indirects may only be unreachable if they have "
"unreachable operands");
}
} else {
shouldBeEqual(curr->type,
curr->sig.results,
curr,
"call_indirect type must match callee return type");
}
}
}
void FunctionValidator::visitConst(Const* curr) {
shouldBeTrue(curr->type.getFeatures() <= getModule()->features,
curr,
"all used features should be allowed");
}
void FunctionValidator::visitLocalGet(LocalGet* curr) {
shouldBeTrue(curr->type.isConcrete(),
curr,
"local.get must have a valid type - check what you provided "
"when you constructed the node");
if (shouldBeTrue(curr->index < getFunction()->getNumLocals(),
curr,
"local.get index must be small enough")) {
shouldBeTrue(curr->type == getFunction()->getLocalType(curr->index),
curr,
"local.get must have proper type");
}
}
void FunctionValidator::visitLocalSet(LocalSet* curr) {
if (shouldBeTrue(curr->index < getFunction()->getNumLocals(),
curr,
"local.set index must be small enough")) {
if (curr->value->type != Type::unreachable) {
if (curr->type != Type::none) { // tee is ok anyhow
shouldBeEqual(getFunction()->getLocalType(curr->index),
curr->type,
curr,
"local.set type must be correct");
}
shouldBeSubType(curr->value->type,
getFunction()->getLocalType(curr->index),
curr,
"local.set's value type must be correct");
}
}
}
void FunctionValidator::visitGlobalGet(GlobalGet* curr) {
if (!info.validateGlobally) {
return;
}
shouldBeTrue(getModule()->getGlobalOrNull(curr->name),
curr,
"global.get name must be valid");
}
void FunctionValidator::visitGlobalSet(GlobalSet* curr) {
if (!info.validateGlobally) {
return;
}
auto* global = getModule()->getGlobalOrNull(curr->name);
if (shouldBeTrue(global,
curr,
"global.set name must be valid (and not an import; imports "
"can't be modified)")) {
shouldBeTrue(global->mutable_, curr, "global.set global must be mutable");
shouldBeSubTypeOrFirstIsUnreachable(
curr->value->type,
global->type,
curr,
"global.set value must have right type");
}
}
void FunctionValidator::visitLoad(Load* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
if (curr->isAtomic) {
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeTrue(curr->type == Type::i32 || curr->type == Type::i64 ||
curr->type == Type::unreachable,
curr,
"Atomic load should be i32 or i64");
}
if (curr->type == Type::v128) {
shouldBeTrue(getModule()->features.hasSIMD(),
curr,
"SIMD operation (SIMD is disabled)");
}
shouldBeFalse(curr->isAtomic && !getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
validateMemBytes(curr->bytes, curr->type, curr);
validateAlignment(curr->align, curr->type, curr->bytes, curr->isAtomic, curr);
shouldBeEqualOrFirstIsUnreachable(
curr->ptr->type, Type(Type::i32), curr, "load pointer type must be i32");
if (curr->isAtomic) {
shouldBeFalse(curr->signed_, curr, "atomic loads must be unsigned");
shouldBeIntOrUnreachable(
curr->type, curr, "atomic loads must be of integers");
}
}
void FunctionValidator::visitStore(Store* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
if (curr->isAtomic) {
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeTrue(curr->valueType == Type::i32 || curr->valueType == Type::i64 ||
curr->valueType == Type::unreachable,
curr,
"Atomic store should be i32 or i64");
}
if (curr->valueType == Type::v128) {
shouldBeTrue(getModule()->features.hasSIMD(),
curr,
"SIMD operation (SIMD is disabled)");
}
shouldBeFalse(curr->isAtomic && !getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
validateMemBytes(curr->bytes, curr->valueType, curr);
validateAlignment(
curr->align, curr->valueType, curr->bytes, curr->isAtomic, curr);
shouldBeEqualOrFirstIsUnreachable(
curr->ptr->type, Type(Type::i32), curr, "store pointer type must be i32");
shouldBeUnequal(curr->value->type,
Type(Type::none),
curr,
"store value type must not be none");
shouldBeEqualOrFirstIsUnreachable(
curr->value->type, curr->valueType, curr, "store value type must match");
if (curr->isAtomic) {
shouldBeIntOrUnreachable(
curr->valueType, curr, "atomic stores must be of integers");
}
}
void FunctionValidator::visitAtomicRMW(AtomicRMW* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeFalse(!getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
validateMemBytes(curr->bytes, curr->type, curr);
shouldBeEqualOrFirstIsUnreachable(curr->ptr->type,
Type(Type::i32),
curr,
"AtomicRMW pointer type must be i32");
shouldBeEqualOrFirstIsUnreachable(curr->type,
curr->value->type,
curr,
"AtomicRMW result type must match operand");
shouldBeIntOrUnreachable(
curr->type, curr, "Atomic operations are only valid on int types");
}
void FunctionValidator::visitAtomicCmpxchg(AtomicCmpxchg* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeFalse(!getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
validateMemBytes(curr->bytes, curr->type, curr);
shouldBeEqualOrFirstIsUnreachable(
curr->ptr->type, Type(Type::i32), curr, "cmpxchg pointer type must be i32");
if (curr->expected->type != Type::unreachable &&
curr->replacement->type != Type::unreachable) {
shouldBeEqual(curr->expected->type,
curr->replacement->type,
curr,
"cmpxchg operand types must match");
}
shouldBeEqualOrFirstIsUnreachable(curr->type,
curr->expected->type,
curr,
"Cmpxchg result type must match expected");
shouldBeEqualOrFirstIsUnreachable(
curr->type,
curr->replacement->type,
curr,
"Cmpxchg result type must match replacement");
shouldBeIntOrUnreachable(curr->expected->type,
curr,
"Atomic operations are only valid on int types");
}
void FunctionValidator::visitAtomicWait(AtomicWait* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeFalse(!getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::i32), curr, "AtomicWait must have type i32");
shouldBeEqualOrFirstIsUnreachable(curr->ptr->type,
Type(Type::i32),
curr,
"AtomicWait pointer type must be i32");
shouldBeIntOrUnreachable(
curr->expected->type, curr, "AtomicWait expected type must be int");
shouldBeEqualOrFirstIsUnreachable(
curr->expected->type,
curr->expectedType,
curr,
"AtomicWait expected type must match operand");
shouldBeEqualOrFirstIsUnreachable(curr->timeout->type,
Type(Type::i64),
curr,
"AtomicWait timeout type must be i64");
}
void FunctionValidator::visitAtomicNotify(AtomicNotify* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeFalse(!getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::i32), curr, "AtomicNotify must have type i32");
shouldBeEqualOrFirstIsUnreachable(curr->ptr->type,
Type(Type::i32),
curr,
"AtomicNotify pointer type must be i32");
shouldBeEqualOrFirstIsUnreachable(
curr->notifyCount->type,
Type(Type::i32),
curr,
"AtomicNotify notifyCount type must be i32");
}
void FunctionValidator::visitAtomicFence(AtomicFence* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(getModule()->features.hasAtomics(),
curr,
"Atomic operation (atomics are disabled)");
shouldBeFalse(!getModule()->memory.shared,
curr,
"Atomic operation with non-shared memory");
shouldBeTrue(curr->order == 0,
curr,
"Currently only sequentially consistent atomics are supported, "
"so AtomicFence's order should be 0");
}
void FunctionValidator::visitSIMDExtract(SIMDExtract* curr) {
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(curr->vec->type,
Type(Type::v128),
curr,
"extract_lane must operate on a v128");
Type lane_t = Type::none;
size_t lanes = 0;
switch (curr->op) {
case ExtractLaneSVecI8x16:
case ExtractLaneUVecI8x16:
lane_t = Type::i32;
lanes = 16;
break;
case ExtractLaneSVecI16x8:
case ExtractLaneUVecI16x8:
lane_t = Type::i32;
lanes = 8;
break;
case ExtractLaneVecI32x4:
lane_t = Type::i32;
lanes = 4;
break;
case ExtractLaneVecI64x2:
lane_t = Type::i64;
lanes = 2;
break;
case ExtractLaneVecF32x4:
lane_t = Type::f32;
lanes = 4;
break;
case ExtractLaneVecF64x2:
lane_t = Type::f64;
lanes = 2;
break;
}
shouldBeEqualOrFirstIsUnreachable(
curr->type,
lane_t,
curr,
"extract_lane must have same type as vector lane");
shouldBeTrue(curr->index < lanes, curr, "invalid lane index");
}
void FunctionValidator::visitSIMDReplace(SIMDReplace* curr) {
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::v128), curr, "replace_lane must have type v128");
shouldBeEqualOrFirstIsUnreachable(curr->vec->type,
Type(Type::v128),
curr,
"replace_lane must operate on a v128");
Type lane_t = Type::none;
size_t lanes = 0;
switch (curr->op) {
case ReplaceLaneVecI8x16:
lane_t = Type::i32;
lanes = 16;
break;
case ReplaceLaneVecI16x8:
lane_t = Type::i32;
lanes = 8;
break;
case ReplaceLaneVecI32x4:
lane_t = Type::i32;
lanes = 4;
break;
case ReplaceLaneVecI64x2:
lane_t = Type::i64;
lanes = 2;
break;
case ReplaceLaneVecF32x4:
lane_t = Type::f32;
lanes = 4;
break;
case ReplaceLaneVecF64x2:
lane_t = Type::f64;
lanes = 2;
break;
}
shouldBeEqualOrFirstIsUnreachable(
curr->value->type, lane_t, curr, "unexpected value type");
shouldBeTrue(curr->index < lanes, curr, "invalid lane index");
}
void FunctionValidator::visitSIMDShuffle(SIMDShuffle* curr) {
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::v128), curr, "v128.shuffle must have type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::v128), curr, "expected operand of type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->right->type, Type(Type::v128), curr, "expected operand of type v128");
for (uint8_t index : curr->mask) {
shouldBeTrue(index < 32, curr, "Invalid lane index in mask");
}
}
void FunctionValidator::visitSIMDTernary(SIMDTernary* curr) {
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::v128), curr, "SIMD ternary must have type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->a->type, Type(Type::v128), curr, "expected operand of type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->b->type, Type(Type::v128), curr, "expected operand of type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->c->type, Type(Type::v128), curr, "expected operand of type v128");
}
void FunctionValidator::visitSIMDShift(SIMDShift* curr) {
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::v128), curr, "vector shift must have type v128");
shouldBeEqualOrFirstIsUnreachable(
curr->vec->type, Type(Type::v128), curr, "expected operand of type v128");
shouldBeEqualOrFirstIsUnreachable(curr->shift->type,
Type(Type::i32),
curr,
"expected shift amount to have type i32");
}
void FunctionValidator::visitSIMDLoad(SIMDLoad* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
shouldBeTrue(
getModule()->features.hasSIMD(), curr, "SIMD operation (SIMD is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::v128), curr, "load_splat must have type v128");
shouldBeEqualOrFirstIsUnreachable(curr->ptr->type,
Type(Type::i32),
curr,
"load_splat address must have type i32");
Type memAlignType = Type::none;
switch (curr->op) {
case LoadSplatVec8x16:
case LoadSplatVec16x8:
case LoadSplatVec32x4:
memAlignType = Type::i32;
break;
case LoadSplatVec64x2:
case LoadExtSVec8x8ToVecI16x8:
case LoadExtUVec8x8ToVecI16x8:
case LoadExtSVec16x4ToVecI32x4:
case LoadExtUVec16x4ToVecI32x4:
case LoadExtSVec32x2ToVecI64x2:
case LoadExtUVec32x2ToVecI64x2:
memAlignType = Type::i64;
break;
}
Index bytes = curr->getMemBytes();
validateAlignment(curr->align, memAlignType, bytes, /*isAtomic=*/false, curr);
}
void FunctionValidator::visitMemoryInit(MemoryInit* curr) {
shouldBeTrue(getModule()->features.hasBulkMemory(),
curr,
"Bulk memory operation (bulk memory is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::none), curr, "memory.init must have type none");
shouldBeEqualOrFirstIsUnreachable(
curr->dest->type, Type(Type::i32), curr, "memory.init dest must be an i32");
shouldBeEqualOrFirstIsUnreachable(curr->offset->type,
Type(Type::i32),
curr,
"memory.init offset must be an i32");
shouldBeEqualOrFirstIsUnreachable(
curr->size->type, Type(Type::i32), curr, "memory.init size must be an i32");
if (!shouldBeTrue(getModule()->memory.exists,
curr,
"Memory operations require a memory")) {
return;
}
shouldBeTrue(curr->segment < getModule()->memory.segments.size(),
curr,
"memory.init segment index out of bounds");
}
void FunctionValidator::visitDataDrop(DataDrop* curr) {
shouldBeTrue(getModule()->features.hasBulkMemory(),
curr,
"Bulk memory operation (bulk memory is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::none), curr, "data.drop must have type none");
if (!shouldBeTrue(getModule()->memory.exists,
curr,
"Memory operations require a memory")) {
return;
}
shouldBeTrue(curr->segment < getModule()->memory.segments.size(),
curr,
"data.drop segment index out of bounds");
}
void FunctionValidator::visitMemoryCopy(MemoryCopy* curr) {
shouldBeTrue(getModule()->features.hasBulkMemory(),
curr,
"Bulk memory operation (bulk memory is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::none), curr, "memory.copy must have type none");
shouldBeEqualOrFirstIsUnreachable(
curr->dest->type, Type(Type::i32), curr, "memory.copy dest must be an i32");
shouldBeEqualOrFirstIsUnreachable(curr->source->type,
Type(Type::i32),
curr,
"memory.copy source must be an i32");
shouldBeEqualOrFirstIsUnreachable(
curr->size->type, Type(Type::i32), curr, "memory.copy size must be an i32");
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
}
void FunctionValidator::visitMemoryFill(MemoryFill* curr) {
shouldBeTrue(getModule()->features.hasBulkMemory(),
curr,
"Bulk memory operation (bulk memory is disabled)");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::none), curr, "memory.fill must have type none");
shouldBeEqualOrFirstIsUnreachable(
curr->dest->type, Type(Type::i32), curr, "memory.fill dest must be an i32");
shouldBeEqualOrFirstIsUnreachable(curr->value->type,
Type(Type::i32),
curr,
"memory.fill value must be an i32");
shouldBeEqualOrFirstIsUnreachable(
curr->size->type, Type(Type::i32), curr, "memory.fill size must be an i32");
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
}
void FunctionValidator::validateMemBytes(uint8_t bytes,
Type type,
Expression* curr) {
switch (type.getSingle()) {
case Type::i32:
shouldBeTrue(bytes == 1 || bytes == 2 || bytes == 4,
curr,
"expected i32 operation to touch 1, 2, or 4 bytes");
break;
case Type::i64:
shouldBeTrue(bytes == 1 || bytes == 2 || bytes == 4 || bytes == 8,
curr,
"expected i64 operation to touch 1, 2, 4, or 8 bytes");
break;
case Type::f32:
shouldBeEqual(
bytes, uint8_t(4), curr, "expected f32 operation to touch 4 bytes");
break;
case Type::f64:
shouldBeEqual(
bytes, uint8_t(8), curr, "expected f64 operation to touch 8 bytes");
break;
case Type::v128:
shouldBeEqual(
bytes, uint8_t(16), curr, "expected v128 operation to touch 16 bytes");
break;
case Type::unreachable:
break;
case Type::funcref:
case Type::anyref:
case Type::nullref:
case Type::exnref:
case Type::none:
WASM_UNREACHABLE("unexpected type");
}
}
void FunctionValidator::visitBinary(Binary* curr) {
if (curr->left->type != Type::unreachable &&
curr->right->type != Type::unreachable) {
shouldBeEqual(curr->left->type,
curr->right->type,
curr,
"binary child types must be equal");
}
switch (curr->op) {
case AddInt32:
case SubInt32:
case MulInt32:
case DivSInt32:
case DivUInt32:
case RemSInt32:
case RemUInt32:
case AndInt32:
case OrInt32:
case XorInt32:
case ShlInt32:
case ShrUInt32:
case ShrSInt32:
case RotLInt32:
case RotRInt32:
case EqInt32:
case NeInt32:
case LtSInt32:
case LtUInt32:
case LeSInt32:
case LeUInt32:
case GtSInt32:
case GtUInt32:
case GeSInt32:
case GeUInt32: {
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::i32), curr, "i32 op");
break;
}
case AddInt64:
case SubInt64:
case MulInt64:
case DivSInt64:
case DivUInt64:
case RemSInt64:
case RemUInt64:
case AndInt64:
case OrInt64:
case XorInt64:
case ShlInt64:
case ShrUInt64:
case ShrSInt64:
case RotLInt64:
case RotRInt64:
case EqInt64:
case NeInt64:
case LtSInt64:
case LtUInt64:
case LeSInt64:
case LeUInt64:
case GtSInt64:
case GtUInt64:
case GeSInt64:
case GeUInt64: {
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::i64), curr, "i64 op");
break;
}
case AddFloat32:
case SubFloat32:
case MulFloat32:
case DivFloat32:
case CopySignFloat32:
case MinFloat32:
case MaxFloat32:
case EqFloat32:
case NeFloat32:
case LtFloat32:
case LeFloat32:
case GtFloat32:
case GeFloat32: {
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::f32), curr, "f32 op");
break;
}
case AddFloat64:
case SubFloat64:
case MulFloat64:
case DivFloat64:
case CopySignFloat64:
case MinFloat64:
case MaxFloat64:
case EqFloat64:
case NeFloat64:
case LtFloat64:
case LeFloat64:
case GtFloat64:
case GeFloat64: {
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::f64), curr, "f64 op");
break;
}
case EqVecI8x16:
case NeVecI8x16:
case LtSVecI8x16:
case LtUVecI8x16:
case LeSVecI8x16:
case LeUVecI8x16:
case GtSVecI8x16:
case GtUVecI8x16:
case GeSVecI8x16:
case GeUVecI8x16:
case EqVecI16x8:
case NeVecI16x8:
case LtSVecI16x8:
case LtUVecI16x8:
case LeSVecI16x8:
case LeUVecI16x8:
case GtSVecI16x8:
case GtUVecI16x8:
case GeSVecI16x8:
case GeUVecI16x8:
case EqVecI32x4:
case NeVecI32x4:
case LtSVecI32x4:
case LtUVecI32x4:
case LeSVecI32x4:
case LeUVecI32x4:
case GtSVecI32x4:
case GtUVecI32x4:
case GeSVecI32x4:
case GeUVecI32x4:
case EqVecF32x4:
case NeVecF32x4:
case LtVecF32x4:
case LeVecF32x4:
case GtVecF32x4:
case GeVecF32x4:
case EqVecF64x2:
case NeVecF64x2:
case LtVecF64x2:
case LeVecF64x2:
case GtVecF64x2:
case GeVecF64x2:
case AndVec128:
case OrVec128:
case XorVec128:
case AndNotVec128:
case AddVecI8x16:
case AddSatSVecI8x16:
case AddSatUVecI8x16:
case SubVecI8x16:
case SubSatSVecI8x16:
case SubSatUVecI8x16:
case MulVecI8x16:
case MinSVecI8x16:
case MinUVecI8x16:
case MaxSVecI8x16:
case MaxUVecI8x16:
case AvgrUVecI8x16:
case AddVecI16x8:
case AddSatSVecI16x8:
case AddSatUVecI16x8:
case SubVecI16x8:
case SubSatSVecI16x8:
case SubSatUVecI16x8:
case MulVecI16x8:
case MinSVecI16x8:
case MinUVecI16x8:
case MaxSVecI16x8:
case MaxUVecI16x8:
case AvgrUVecI16x8:
case AddVecI32x4:
case SubVecI32x4:
case MulVecI32x4:
case MinSVecI32x4:
case MinUVecI32x4:
case MaxSVecI32x4:
case MaxUVecI32x4:
case DotSVecI16x8ToVecI32x4:
case AddVecI64x2:
case SubVecI64x2:
case AddVecF32x4:
case SubVecF32x4:
case MulVecF32x4:
case DivVecF32x4:
case MinVecF32x4:
case MaxVecF32x4:
case AddVecF64x2:
case SubVecF64x2:
case MulVecF64x2:
case DivVecF64x2:
case MinVecF64x2:
case MaxVecF64x2:
case NarrowSVecI16x8ToVecI8x16:
case NarrowUVecI16x8ToVecI8x16:
case NarrowSVecI32x4ToVecI16x8:
case NarrowUVecI32x4ToVecI16x8:
case SwizzleVec8x16: {
shouldBeEqualOrFirstIsUnreachable(
curr->left->type, Type(Type::v128), curr, "v128 op");
shouldBeEqualOrFirstIsUnreachable(
curr->right->type, Type(Type::v128), curr, "v128 op");
break;
}
case InvalidBinary:
WASM_UNREACHABLE("invliad binary op");
}
shouldBeTrue(Features::get(curr->op) <= getModule()->features,
curr,
"all used features should be allowed");
}
void FunctionValidator::visitUnary(Unary* curr) {
shouldBeUnequal(curr->value->type,
Type(Type::none),
curr,
"unaries must not receive a none as their input");
if (curr->value->type == Type::unreachable) {
return; // nothing to check
}
switch (curr->op) {
case ClzInt32:
case CtzInt32:
case PopcntInt32: {
shouldBeEqual(curr->value->type,
Type(Type::i32),
curr,
"i32 unary value type must be correct");
break;
}
case ClzInt64:
case CtzInt64:
case PopcntInt64: {
shouldBeEqual(curr->value->type,
Type(Type::i64),
curr,
"i64 unary value type must be correct");
break;
}
case NegFloat32:
case AbsFloat32:
case CeilFloat32:
case FloorFloat32:
case TruncFloat32:
case NearestFloat32:
case SqrtFloat32: {
shouldBeEqual(curr->value->type,
Type(Type::f32),
curr,
"f32 unary value type must be correct");
break;
}
case NegFloat64:
case AbsFloat64:
case CeilFloat64:
case FloorFloat64:
case TruncFloat64:
case NearestFloat64:
case SqrtFloat64: {
shouldBeEqual(curr->value->type,
Type(Type::f64),
curr,
"f64 unary value type must be correct");
break;
}
case EqZInt32: {
shouldBeTrue(
curr->value->type == Type::i32, curr, "i32.eqz input must be i32");
break;
}
case EqZInt64: {
shouldBeTrue(curr->value->type == Type(Type::i64),
curr,
"i64.eqz input must be i64");
break;
}
case ExtendSInt32:
case ExtendUInt32:
case ExtendS8Int32:
case ExtendS16Int32: {
shouldBeEqual(curr->value->type,
Type(Type::i32),
curr,
"extend type must be correct");
break;
}
case ExtendS8Int64:
case ExtendS16Int64:
case ExtendS32Int64: {
shouldBeEqual(curr->value->type,
Type(Type::i64),
curr,
"extend type must be correct");
break;
}
case WrapInt64: {
shouldBeEqual(
curr->value->type, Type(Type::i64), curr, "wrap type must be correct");
break;
}
case TruncSFloat32ToInt32:
case TruncSFloat32ToInt64:
case TruncUFloat32ToInt32:
case TruncUFloat32ToInt64: {
shouldBeEqual(
curr->value->type, Type(Type::f32), curr, "trunc type must be correct");
break;
}
case TruncSatSFloat32ToInt32:
case TruncSatSFloat32ToInt64:
case TruncSatUFloat32ToInt32:
case TruncSatUFloat32ToInt64: {
shouldBeEqual(
curr->value->type, Type(Type::f32), curr, "trunc type must be correct");
break;
}
case TruncSFloat64ToInt32:
case TruncSFloat64ToInt64:
case TruncUFloat64ToInt32:
case TruncUFloat64ToInt64: {
shouldBeEqual(
curr->value->type, Type(Type::f64), curr, "trunc type must be correct");
break;
}
case TruncSatSFloat64ToInt32:
case TruncSatSFloat64ToInt64:
case TruncSatUFloat64ToInt32:
case TruncSatUFloat64ToInt64: {
shouldBeEqual(
curr->value->type, Type(Type::f64), curr, "trunc type must be correct");
break;
}
case ReinterpretFloat32: {
shouldBeEqual(curr->value->type,
Type(Type::f32),
curr,
"reinterpret/f32 type must be correct");
break;
}
case ReinterpretFloat64: {
shouldBeEqual(curr->value->type,
Type(Type::f64),
curr,
"reinterpret/f64 type must be correct");
break;
}
case ConvertUInt32ToFloat32:
case ConvertUInt32ToFloat64:
case ConvertSInt32ToFloat32:
case ConvertSInt32ToFloat64: {
shouldBeEqual(curr->value->type,
Type(Type::i32),
curr,
"convert type must be correct");
break;
}
case ConvertUInt64ToFloat32:
case ConvertUInt64ToFloat64:
case ConvertSInt64ToFloat32:
case ConvertSInt64ToFloat64: {
shouldBeEqual(curr->value->type,
Type(Type::i64),
curr,
"convert type must be correct");
break;
}
case PromoteFloat32: {
shouldBeEqual(curr->value->type,
Type(Type::f32),
curr,
"promote type must be correct");
break;
}
case DemoteFloat64: {
shouldBeEqual(curr->value->type,
Type(Type::f64),
curr,
"demote type must be correct");
break;
}
case ReinterpretInt32: {
shouldBeEqual(curr->value->type,
Type(Type::i32),
curr,
"reinterpret/i32 type must be correct");
break;
}
case ReinterpretInt64: {
shouldBeEqual(curr->value->type,
Type(Type::i64),
curr,
"reinterpret/i64 type must be correct");
break;
}
case SplatVecI8x16:
case SplatVecI16x8:
case SplatVecI32x4:
shouldBeEqual(
curr->type, Type(Type::v128), curr, "expected splat to have v128 type");
shouldBeEqual(
curr->value->type, Type(Type::i32), curr, "expected i32 splat value");
break;
case SplatVecI64x2:
shouldBeEqual(
curr->type, Type(Type::v128), curr, "expected splat to have v128 type");
shouldBeEqual(
curr->value->type, Type(Type::i64), curr, "expected i64 splat value");
break;
case SplatVecF32x4:
shouldBeEqual(
curr->type, Type(Type::v128), curr, "expected splat to have v128 type");
shouldBeEqual(
curr->value->type, Type(Type::f32), curr, "expected f32 splat value");
break;
case SplatVecF64x2:
shouldBeEqual(
curr->type, Type(Type::v128), curr, "expected splat to have v128 type");
shouldBeEqual(
curr->value->type, Type(Type::f64), curr, "expected f64 splat value");
break;
case NotVec128:
case NegVecI8x16:
case NegVecI16x8:
case NegVecI32x4:
case NegVecI64x2:
case AbsVecF32x4:
case NegVecF32x4:
case SqrtVecF32x4:
case AbsVecF64x2:
case NegVecF64x2:
case SqrtVecF64x2:
case TruncSatSVecF32x4ToVecI32x4:
case TruncSatUVecF32x4ToVecI32x4:
case TruncSatSVecF64x2ToVecI64x2:
case TruncSatUVecF64x2ToVecI64x2:
case ConvertSVecI32x4ToVecF32x4:
case ConvertUVecI32x4ToVecF32x4:
case ConvertSVecI64x2ToVecF64x2:
case ConvertUVecI64x2ToVecF64x2:
case WidenLowSVecI8x16ToVecI16x8:
case WidenHighSVecI8x16ToVecI16x8:
case WidenLowUVecI8x16ToVecI16x8:
case WidenHighUVecI8x16ToVecI16x8:
case WidenLowSVecI16x8ToVecI32x4:
case WidenHighSVecI16x8ToVecI32x4:
case WidenLowUVecI16x8ToVecI32x4:
case WidenHighUVecI16x8ToVecI32x4:
shouldBeEqual(curr->type, Type(Type::v128), curr, "expected v128 type");
shouldBeEqual(
curr->value->type, Type(Type::v128), curr, "expected v128 operand");
break;
case AnyTrueVecI8x16:
case AllTrueVecI8x16:
case AnyTrueVecI16x8:
case AllTrueVecI16x8:
case AnyTrueVecI32x4:
case AllTrueVecI32x4:
case AnyTrueVecI64x2:
case AllTrueVecI64x2:
shouldBeEqual(curr->type,
Type(Type::i32),
curr,
"expected boolean reduction to have i32 type");
shouldBeEqual(
curr->value->type, Type(Type::v128), curr, "expected v128 operand");
break;
case InvalidUnary:
WASM_UNREACHABLE("invalid unary op");
}
shouldBeTrue(Features::get(curr->op) <= getModule()->features,
curr,
"all used features should be allowed");
}
void FunctionValidator::visitSelect(Select* curr) {
shouldBeUnequal(
curr->ifFalse->type, Type(Type::none), curr, "select right must be valid");
shouldBeUnequal(
curr->type, Type(Type::none), curr, "select type must be valid");
shouldBeTrue(curr->condition->type == Type::unreachable ||
curr->condition->type == Type::i32,
curr,
"select condition must be valid");
if (curr->type != Type::unreachable) {
shouldBeTrue(Type::isSubType(curr->ifTrue->type, curr->type),
curr,
"select's left expression must be subtype of select's type");
shouldBeTrue(Type::isSubType(curr->ifFalse->type, curr->type),
curr,
"select's right expression must be subtype of select's type");
}
}
void FunctionValidator::visitDrop(Drop* curr) {
shouldBeTrue(curr->value->type.isConcrete() ||
curr->value->type == Type::unreachable,
curr,
"can only drop a valid value");
}
void FunctionValidator::visitReturn(Return* curr) {
returnTypes.insert(curr->value ? curr->value->type : Type::none);
}
void FunctionValidator::visitHost(Host* curr) {
shouldBeTrue(
getModule()->memory.exists, curr, "Memory operations require a memory");
switch (curr->op) {
case MemoryGrow: {
shouldBeEqual(curr->operands.size(),
size_t(1),
curr,
"memory.grow must have 1 operand");
shouldBeEqualOrFirstIsUnreachable(curr->operands[0]->type,
Type(Type::i32),
curr,
"memory.grow must have i32 operand");
break;
}
case MemorySize:
break;
}
}
void FunctionValidator::visitRefIsNull(RefIsNull* curr) {
shouldBeTrue(curr->value->type == Type::unreachable ||
curr->value->type.isRef(),
curr->value,
"ref.is_null's argument should be a reference type");
}
void FunctionValidator::visitRefFunc(RefFunc* curr) {
auto* func = getModule()->getFunctionOrNull(curr->func);
shouldBeTrue(!!func, curr, "function argument of ref.func must exist");
}
void FunctionValidator::visitTry(Try* curr) {
if (curr->type != Type::unreachable) {
shouldBeSubTypeOrFirstIsUnreachable(
curr->body->type,
curr->type,
curr->body,
"try's type does not match try body's type");
shouldBeSubTypeOrFirstIsUnreachable(
curr->catchBody->type,
curr->type,
curr->catchBody,
"try's type does not match catch's body type");
} else {
shouldBeEqual(curr->body->type,
Type(Type::unreachable),
curr,
"unreachable try-catch must have unreachable try body");
shouldBeEqual(curr->catchBody->type,
Type(Type::unreachable),
curr,
"unreachable try-catch must have unreachable catch body");
}
}
void FunctionValidator::visitThrow(Throw* curr) {
if (!info.validateGlobally) {
return;
}
shouldBeEqual(curr->type,
Type(Type::unreachable),
curr,
"throw's type must be unreachable");
auto* event = getModule()->getEventOrNull(curr->event);
if (!shouldBeTrue(!!event, curr, "throw's event must exist")) {
return;
}
if (!shouldBeTrue(curr->operands.size() == event->sig.params.size(),
curr,
"event's param numbers must match")) {
return;
}
const std::vector<Type>& paramTypes = event->sig.params.expand();
for (size_t i = 0; i < curr->operands.size(); i++) {
if (!shouldBeSubTypeOrFirstIsUnreachable(curr->operands[i]->type,
paramTypes[i],
curr->operands[i],
"event param types must match") &&
!info.quiet) {
getStream() << "(on argument " << i << ")\n";
}
}
}
void FunctionValidator::visitRethrow(Rethrow* curr) {
shouldBeEqual(curr->type,
Type(Type::unreachable),
curr,
"rethrow's type must be unreachable");
shouldBeSubTypeOrFirstIsUnreachable(
curr->exnref->type,
Type::exnref,
curr->exnref,
"rethrow's argument must be exnref type or its subtype");
}
void FunctionValidator::visitBrOnExn(BrOnExn* curr) {
Event* event = getModule()->getEventOrNull(curr->event);
shouldBeTrue(event != nullptr, curr, "br_on_exn's event must exist");
shouldBeTrue(event->sig.params == curr->sent,
curr,
"br_on_exn's event params and event's params are different");
noteBreak(curr->name, curr->sent, curr);
shouldBeSubTypeOrFirstIsUnreachable(
curr->exnref->type,
Type::exnref,
curr,
"br_on_exn's argument must be unreachable or exnref type or its subtype");
if (curr->exnref->type == Type::unreachable) {
shouldBeTrue(curr->type == Type::unreachable,
curr,
"If exnref argument's type is unreachable, br_on_exn should "
"be unreachable too");
} else {
shouldBeTrue(curr->type == Type::exnref,
curr,
"br_on_exn's type should be exnref unless its exnref argument "
"is unreachable");
}
}
void FunctionValidator::visitFunction(Function* curr) {
shouldBeTrue(!curr->sig.results.isMulti(),
curr->body,
"Multivalue functions not allowed yet");
FeatureSet features;
for (auto type : curr->sig.params.expand()) {
features |= type.getFeatures();
shouldBeTrue(type.isConcrete(), curr, "params must be concretely typed");
}
for (auto type : curr->sig.results.expand()) {
features |= type.getFeatures();
shouldBeTrue(type.isConcrete(), curr, "results must be concretely typed");
}
for (auto type : curr->vars) {
features |= type.getFeatures();
shouldBeTrue(type.isConcrete(), curr, "vars must be concretely typed");
}
shouldBeTrue(features <= getModule()->features,
curr,
"all used types should be allowed");
// if function has no result, it is ignored
// if body is unreachable, it might be e.g. a return
shouldBeSubTypeOrFirstIsUnreachable(
curr->body->type,
curr->sig.results,
curr->body,
"function body type must match, if function returns");
for (Type returnType : returnTypes) {
shouldBeSubTypeOrFirstIsUnreachable(
returnType,
curr->sig.results,
curr->body,
"function result must match, if function has returns");
}
shouldBeTrue(
breakInfos.empty(), curr->body, "all named break targets must exist");
returnTypes.clear();
labelNames.clear();
// validate optional local names
std::set<Name> seen;
for (auto& pair : curr->localNames) {
Name name = pair.second;
shouldBeTrue(seen.insert(name).second, name, "local names must be unique");
}
}
static bool checkOffset(Expression* curr, Address add, Address max) {
if (curr->is<GlobalGet>()) {
return true;
}
auto* c = curr->dynCast<Const>();
if (!c) {
return false;
}
uint64_t raw = c->value.getInteger();
if (raw > std::numeric_limits<Address::address_t>::max()) {
return false;
}
if (raw + uint64_t(add) > std::numeric_limits<Address::address_t>::max()) {
return false;
}
Address offset = raw;
return offset + add <= max;
}
void FunctionValidator::validateAlignment(
size_t align, Type type, Index bytes, bool isAtomic, Expression* curr) {
if (isAtomic) {
shouldBeEqual(align,
(size_t)bytes,
curr,
"atomic accesses must have natural alignment");
return;
}
switch (align) {
case 1:
case 2:
case 4:
case 8:
case 16:
break;
default: {
info.fail("bad alignment: " + std::to_string(align), curr, getFunction());
break;
}
}
shouldBeTrue(align <= bytes, curr, "alignment must not exceed natural");
switch (type.getSingle()) {
case Type::i32:
case Type::f32: {
shouldBeTrue(align <= 4, curr, "alignment must not exceed natural");
break;
}
case Type::i64:
case Type::f64: {
shouldBeTrue(align <= 8, curr, "alignment must not exceed natural");
break;
}
case Type::v128:
case Type::unreachable:
break;
case Type::funcref:
case Type::anyref:
case Type::nullref:
case Type::exnref:
case Type::none:
WASM_UNREACHABLE("invalid type");
}
}
static void validateBinaryenIR(Module& wasm, ValidationInfo& info) {
struct BinaryenIRValidator
: public PostWalker<BinaryenIRValidator,
UnifiedExpressionVisitor<BinaryenIRValidator>> {
ValidationInfo& info;
std::unordered_set<Expression*> seen;
BinaryenIRValidator(ValidationInfo& info) : info(info) {}
void visitExpression(Expression* curr) {
auto scope = getFunction() ? getFunction()->name : Name("(global scope)");
// check if a node type is 'stale', i.e., we forgot to finalize() the
// node.
auto oldType = curr->type;
ReFinalizeNode().visit(curr);
auto newType = curr->type;
if (newType != oldType) {
// We accept concrete => undefined,
// e.g.
//
// (drop (block (result i32) (unreachable)))
//
// The block has an added type, not derived from the ast itself, so it
// is ok for it to be either i32 or unreachable.
if (!Type::isSubType(newType, oldType) &&
!(oldType.isConcrete() && newType == Type::unreachable)) {
std::ostringstream ss;
ss << "stale type found in " << scope << " on " << curr
<< "\n(marked as " << oldType << ", should be " << newType
<< ")\n";
info.fail(ss.str(), curr, getFunction());
}
curr->type = oldType;
}
// check if a node is a duplicate - expressions must not be seen more than
// once
bool inserted;
std::tie(std::ignore, inserted) = seen.insert(curr);
if (!inserted) {
std::ostringstream ss;
ss << "expression seen more than once in the tree in " << scope
<< " on " << curr << '\n';
info.fail(ss.str(), curr, getFunction());
}
}
};
BinaryenIRValidator binaryenIRValidator(info);
binaryenIRValidator.walkModule(&wasm);
}
// Main validator class
static void validateImports(Module& module, ValidationInfo& info) {
ModuleUtils::iterImportedFunctions(module, [&](Function* curr) {
if (info.validateWeb) {
for (Type param : curr->sig.params.expand()) {
info.shouldBeUnequal(param,
Type(Type::i64),
curr->name,
"Imported function must not have i64 parameters");
}
for (Type result : curr->sig.results.expand()) {
info.shouldBeUnequal(result,
Type(Type::i64),
curr->name,
"Imported function must not have i64 results");
}
}
});
if (!module.features.hasMutableGlobals()) {
ModuleUtils::iterImportedGlobals(module, [&](Global* curr) {
info.shouldBeFalse(
curr->mutable_, curr->name, "Imported global cannot be mutable");
});
}
}
static void validateExports(Module& module, ValidationInfo& info) {
for (auto& curr : module.exports) {
if (curr->kind == ExternalKind::Function) {
if (info.validateWeb) {
Function* f = module.getFunction(curr->value);
for (auto param : f->sig.params.expand()) {
info.shouldBeUnequal(
param,
Type(Type::i64),
f->name,
"Exported function must not have i64 parameters");
}
for (auto result : f->sig.results.expand()) {
info.shouldBeUnequal(result,
Type(Type::i64),
f->name,
"Exported function must not have i64 results");
}
}
} else if (curr->kind == ExternalKind::Global &&
!module.features.hasMutableGlobals()) {
if (Global* g = module.getGlobalOrNull(curr->value)) {
info.shouldBeFalse(
g->mutable_, g->name, "Exported global cannot be mutable");
}
}
}
std::unordered_set<Name> exportNames;
for (auto& exp : module.exports) {
Name name = exp->value;
if (exp->kind == ExternalKind::Function) {
info.shouldBeTrue(module.getFunctionOrNull(name),
name,
"module function exports must be found");
} else if (exp->kind == ExternalKind::Global) {
info.shouldBeTrue(module.getGlobalOrNull(name),
name,
"module global exports must be found");
} else if (exp->kind == ExternalKind::Table) {
info.shouldBeTrue(name == Name("0") || name == module.table.name,
name,
"module table exports must be found");
} else if (exp->kind == ExternalKind::Memory) {
info.shouldBeTrue(name == Name("0") || name == module.memory.name,
name,
"module memory exports must be found");
} else if (exp->kind == ExternalKind::Event) {
info.shouldBeTrue(module.getEventOrNull(name),
name,
"module event exports must be found");
} else {
WASM_UNREACHABLE("invalid ExternalKind");
}
Name exportName = exp->name;
info.shouldBeFalse(exportNames.count(exportName) > 0,
exportName,
"module exports must be unique");
exportNames.insert(exportName);
}
}
static void validateGlobals(Module& module, ValidationInfo& info) {
ModuleUtils::iterDefinedGlobals(module, [&](Global* curr) {
info.shouldBeTrue(curr->type.getFeatures() <= module.features,
curr->name,
"all used types should be allowed");
info.shouldBeTrue(
curr->init != nullptr, curr->name, "global init must be non-null");
assert(curr->init);
info.shouldBeTrue(GlobalUtils::canInitializeGlobal(curr->init),
curr->name,
"global init must be valid");
if (!info.shouldBeSubType(curr->init->type,
curr->type,
curr->init,
"global init must have correct type") &&
!info.quiet) {
info.getStream(nullptr) << "(on global " << curr->name << ")\n";
}
});
}
static void validateMemory(Module& module, ValidationInfo& info) {
auto& curr = module.memory;
info.shouldBeFalse(
curr.initial > curr.max, "memory", "memory max >= initial");
info.shouldBeTrue(curr.initial <= Memory::kMaxSize,
"memory",
"initial memory must be <= 4GB");
info.shouldBeTrue(!curr.hasMax() || curr.max <= Memory::kMaxSize,
"memory",
"max memory must be <= 4GB, or unlimited");
info.shouldBeTrue(!curr.shared || curr.hasMax(),
"memory",
"shared memory must have max size");
if (curr.shared) {
info.shouldBeTrue(module.features.hasAtomics(),
"memory",
"memory is shared, but atomics are disabled");
}
for (auto& segment : curr.segments) {
Index size = segment.data.size();
if (segment.isPassive) {
info.shouldBeTrue(module.features.hasBulkMemory(),
segment.offset,
"nonzero segment flags (bulk memory is disabled)");
info.shouldBeEqual(segment.offset,
(Expression*)nullptr,
segment.offset,
"passive segment should not have an offset");
} else {
if (!info.shouldBeEqual(segment.offset->type,
Type(Type::i32),
segment.offset,
"segment offset should be i32")) {
continue;
}
info.shouldBeTrue(checkOffset(segment.offset,
segment.data.size(),
curr.initial * Memory::kPageSize),
segment.offset,
"segment offset should be reasonable");
if (segment.offset->is<Const>()) {
Index start = segment.offset->cast<Const>()->value.geti32();
Index end = start + size;
info.shouldBeTrue(end <= curr.initial * Memory::kPageSize,
segment.data.size(),
"segment size should fit in memory (end)");
}
}
// If the memory is imported we don't actually know its initial size.
// Specifically wasm dll's import a zero sized memory which is perfectly
// valid.
if (!curr.imported()) {
info.shouldBeTrue(size <= curr.initial * Memory::kPageSize,
segment.data.size(),
"segment size should fit in memory (initial)");
}
}
}
static void validateTable(Module& module, ValidationInfo& info) {
auto& curr = module.table;
for (auto& segment : curr.segments) {
info.shouldBeEqual(segment.offset->type,
Type(Type::i32),
segment.offset,
"segment offset should be i32");
info.shouldBeTrue(checkOffset(segment.offset,
segment.data.size(),
module.table.initial * Table::kPageSize),
segment.offset,
"segment offset should be reasonable");
for (auto name : segment.data) {
info.shouldBeTrue(
module.getFunctionOrNull(name), name, "segment name should be valid");
}
}
}
static void validateEvents(Module& module, ValidationInfo& info) {
if (!module.events.empty()) {
info.shouldBeTrue(module.features.hasExceptionHandling(),
module.events[0]->name,
"Module has events (event-handling is disabled)");
}
for (auto& curr : module.events) {
info.shouldBeEqual(curr->attribute,
(unsigned)0,
curr->attribute,
"Currently only attribute 0 is supported");
info.shouldBeEqual(curr->sig.results,
Type(Type::none),
curr->name,
"Event type's result type should be none");
for (auto type : curr->sig.params.expand()) {
info.shouldBeTrue(type.isConcrete(),
curr->name,
"Values in an event should have concrete types");
}
}
}
static void validateModule(Module& module, ValidationInfo& info) {
// start
if (module.start.is()) {
auto func = module.getFunctionOrNull(module.start);
if (info.shouldBeTrue(
func != nullptr, module.start, "start must be found")) {
info.shouldBeTrue(func->sig.params == Type::none,
module.start,
"start must have 0 params");
info.shouldBeTrue(func->sig.results == Type::none,
module.start,
"start must not return a value");
}
}
}
// TODO: If we want the validator to be part of libwasm rather than libpasses,
// then Using PassRunner::getPassDebug causes a circular dependence. We should
// fix that, perhaps by moving some of the pass infrastructure into libsupport.
bool WasmValidator::validate(Module& module, Flags flags) {
ValidationInfo info;
info.validateWeb = (flags & Web) != 0;
info.validateGlobally = (flags & Globally) != 0;
info.quiet = (flags & Quiet) != 0;
// parallel wasm logic validation
PassRunner runner(&module);
FunctionValidator(&info).run(&runner, &module);
// validate globally
if (info.validateGlobally) {
validateImports(module, info);
validateExports(module, info);
validateGlobals(module, info);
validateMemory(module, info);
validateTable(module, info);
validateEvents(module, info);
validateModule(module, info);
}
// validate additional internal IR details when in pass-debug mode
if (PassRunner::getPassDebug()) {
validateBinaryenIR(module, info);
}
// print all the data
if (!info.valid.load() && !info.quiet) {
for (auto& func : module.functions) {
std::cerr << info.getStream(func.get()).str();
}
std::cerr << info.getStream(nullptr).str();
}
return info.valid.load();
}
} // namespace wasm