blob: 38bc263e9c5a9b0672171ce9a709b4c38094a0cf [file] [log] [blame] [edit]
/*
* Copyright 2022 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.
*/
#ifndef wasm_ir_possible_contents_h
#define wasm_ir_possible_contents_h
#include <variant>
#include "ir/possible-constant.h"
#include "ir/subtypes.h"
#include "support/small_vector.h"
#include "wasm-builder.h"
#include "wasm.h"
namespace wasm {
//
// PossibleContents represents the possible contents at a particular location
// (such as in a local or in a function parameter). This is a little similar to
// PossibleConstantValues, but considers more types of contents than constant
// values - in particular, it can track types to some extent.
//
// The specific contents this can vary over are:
//
// * None: No possible value.
//
// * Literal: One possible constant value like an i32 of 42.
//
// * Global: The name of a global whose value is here. We do not know
// the actual value at compile time, but we know it is equal
// to that global. Typically we can only infer this for
// immutable globals.
//
// * ExactType: Any possible value of a specific exact type - *not*
// including subtypes. For example, (struct.new $Foo) has
// ExactType contents of type $Foo.
// If the type here is nullable then null is also allowed.
// TODO: Add ConeType, which would include subtypes.
// TODO: Add ExactTypePlusContents or such, which would be
// used on e.g. a struct.new with an immutable field
// to which we assign a constant: not only do we know
// the exact type, but also certain field's values.
//
// * Many: Anything else. Many things are possible here, and we do
// not track what they might be, so we must assume the worst
// in the calling code.
//
class PossibleContents {
struct None : public std::monostate {};
struct GlobalInfo {
Name name;
// The type of the global in the module. We stash this here so that we do
// not need to pass around a module all the time.
// TODO: could we save size in this variant if we did pass around the
// module?
Type type;
bool operator==(const GlobalInfo& other) const {
return name == other.name && type == other.type;
}
};
using ExactType = Type;
struct Many : public std::monostate {};
// TODO: This is similar to the variant in PossibleConstantValues, and perhaps
// we could share code, but extending a variant using template magic may
// not be worthwhile. Another option might be to make PCV inherit from
// this and disallow ExactType etc., but PCV might get slower.
using Variant = std::variant<None, Literal, GlobalInfo, ExactType, Many>;
Variant value;
public:
PossibleContents() : value(None()) {}
PossibleContents(const PossibleContents& other) = default;
template<typename T> explicit PossibleContents(T val) : value(val) {}
// Most users will use one of the following static functions to construct a
// new instance:
static PossibleContents none() { return PossibleContents{None()}; }
static PossibleContents literal(Literal c) { return PossibleContents{c}; }
static PossibleContents global(Name name, Type type) {
return PossibleContents{GlobalInfo{name, type}};
}
static PossibleContents exactType(Type type) {
return PossibleContents{ExactType(type)};
}
static PossibleContents many() { return PossibleContents{Many()}; }
PossibleContents& operator=(const PossibleContents& other) = default;
bool operator==(const PossibleContents& other) const {
return value == other.value;
}
bool operator!=(const PossibleContents& other) const {
return !(*this == other);
}
// Combine the information in a given PossibleContents to this one. The
// contents here will then include whatever content was possible in |other|.
void combine(const PossibleContents& other);
bool isNone() const { return std::get_if<None>(&value); }
bool isLiteral() const { return std::get_if<Literal>(&value); }
bool isGlobal() const { return std::get_if<GlobalInfo>(&value); }
bool isExactType() const { return std::get_if<Type>(&value); }
bool isMany() const { return std::get_if<Many>(&value); }
Literal getLiteral() const {
assert(isLiteral());
return std::get<Literal>(value);
}
Name getGlobal() const {
assert(isGlobal());
return std::get<GlobalInfo>(value).name;
}
bool isNull() const { return isLiteral() && getLiteral().isNull(); }
// Return the relevant type here. Note that the *meaning* of the type varies
// by the contents: type $foo of a global means that type or any subtype, as a
// subtype might be written to it, while type $foo of a Literal or an
// ExactType means that type and nothing else; see hasExactType().
//
// If no type is possible, return unreachable; if many types are, return none.
Type getType() const {
if (auto* literal = std::get_if<Literal>(&value)) {
return literal->type;
} else if (auto* global = std::get_if<GlobalInfo>(&value)) {
return global->type;
} else if (auto* type = std::get_if<Type>(&value)) {
return *type;
} else if (std::get_if<None>(&value)) {
return Type::unreachable;
} else if (std::get_if<Many>(&value)) {
return Type::none;
} else {
WASM_UNREACHABLE("bad value");
}
}
// Returns whether the type we can report here is exact, that is, nothing of a
// strict subtype might show up - the contents here have an exact type.
//
// This is different from isExactType() which checks if all we know about the
// contents here is their exact type. Specifically, we may know both an exact
// type and also more than just that, which is the case with a Literal.
//
// This returns false for None and Many, for whom it is not well-defined.
bool hasExactType() const { return isExactType() || isLiteral(); }
// Whether we can make an Expression* for this containing the proper contents.
// We can do that for a Literal (emitting a Const or RefFunc etc.) or a
// Global (emitting a GlobalGet), but not for anything else yet.
bool canMakeExpression() const { return isLiteral() || isGlobal(); }
Expression* makeExpression(Module& wasm) {
assert(canMakeExpression());
Builder builder(wasm);
if (isLiteral()) {
return builder.makeConstantExpression(getLiteral());
} else {
auto name = getGlobal();
return builder.makeGlobalGet(name, wasm.getGlobal(name)->type);
}
}
size_t hash() const {
// Encode this using three bits for the variant type, then the rest of the
// contents.
if (isNone()) {
return 0;
} else if (isLiteral()) {
return size_t(1) | (std::hash<Literal>()(getLiteral()) << 3);
} else if (isGlobal()) {
return size_t(2) | (std::hash<Name>()(getGlobal()) << 3);
} else if (isExactType()) {
return size_t(3) | (std::hash<Type>()(getType()) << 3);
} else if (isMany()) {
return 4;
} else {
WASM_UNREACHABLE("bad variant");
}
}
void dump(std::ostream& o, Module* wasm = nullptr) const {
o << '[';
if (isNone()) {
o << "None";
} else if (isLiteral()) {
o << "Literal " << getLiteral();
auto t = getType();
if (t.isRef()) {
auto h = t.getHeapType();
o << " HT: " << h;
}
} else if (isGlobal()) {
o << "GlobalInfo $" << getGlobal();
} else if (isExactType()) {
o << "ExactType " << getType();
auto t = getType();
if (t.isRef()) {
auto h = t.getHeapType();
o << " HT: " << h;
if (wasm && wasm->typeNames.count(h)) {
o << " $" << wasm->typeNames[h].name;
}
if (t.isNullable()) {
o << " null";
}
}
} else if (isMany()) {
o << "Many";
} else {
WASM_UNREACHABLE("bad variant");
}
o << ']';
}
};
// The various *Location structs (ExpressionLocation, ResultLocation, etc.)
// describe particular locations where content can appear.
// The location of a specific IR expression.
struct ExpressionLocation {
Expression* expr;
// If this expression contains a tuple then each index in the tuple will have
// its own location with a corresponding tupleIndex. If this is not a tuple
// then we only use tupleIndex 0.
Index tupleIndex;
bool operator==(const ExpressionLocation& other) const {
return expr == other.expr && tupleIndex == other.tupleIndex;
}
};
// The location of one of the results of a function.
struct ResultLocation {
Function* func;
Index index;
bool operator==(const ResultLocation& other) const {
return func == other.func && index == other.index;
}
};
// The location of one of the locals in a function (either a param or a var).
// TODO: would separating params from vars help? (SSA might be enough)
struct LocalLocation {
Function* func;
// The index of the local.
Index index;
// As in ExpressionLocation, the index inside the tuple, or 0 if not a tuple.
Index tupleIndex;
bool operator==(const LocalLocation& other) const {
return func == other.func && index == other.index &&
tupleIndex == other.tupleIndex;
}
};
// The location of a break target in a function, identified by its name.
struct BreakTargetLocation {
Function* func;
Name target;
// As in ExpressionLocation, the index inside the tuple, or 0 if not a tuple.
// That is, if the branch target has a tuple type, then each branch to that
// location sends a tuple, and we'll have a separate BreakTargetLocation for
// each, indexed by the index in the tuple that the branch sends.
Index tupleIndex;
bool operator==(const BreakTargetLocation& other) const {
return func == other.func && target == other.target &&
tupleIndex == other.tupleIndex;
}
};
// The location of a global in the module.
struct GlobalLocation {
Name name;
bool operator==(const GlobalLocation& other) const {
return name == other.name;
}
};
// The location of one of the parameters in a function signature.
struct SignatureParamLocation {
HeapType type;
Index index;
bool operator==(const SignatureParamLocation& other) const {
return type == other.type && index == other.index;
}
};
// The location of one of the results in a function signature.
struct SignatureResultLocation {
HeapType type;
Index index;
bool operator==(const SignatureResultLocation& other) const {
return type == other.type && index == other.index;
}
};
// The location of contents in a struct or array (i.e., things that can fit in a
// dataref). Note that this is specific to this type - it does not include data
// about subtypes or supertypes.
struct DataLocation {
HeapType type;
// The index of the field in a struct, or 0 for an array (where we do not
// attempt to differentiate by index).
Index index;
bool operator==(const DataLocation& other) const {
return type == other.type && index == other.index;
}
};
// The location of anything written to a particular tag.
struct TagLocation {
Name tag;
// If the tag has more than one element, we'll have a separate TagLocation for
// each, with corresponding indexes. If the tag has just one element we'll
// only have one TagLocation with index 0.
Index tupleIndex;
bool operator==(const TagLocation& other) const {
return tag == other.tag && tupleIndex == other.tupleIndex;
}
};
// A null value. This is used as the location of the default value of a var in a
// function, a null written to a struct field in struct.new_with_default, etc.
struct NullLocation {
Type type;
bool operator==(const NullLocation& other) const {
return type == other.type;
}
};
// A special type of location that does not refer to something concrete in the
// wasm, but is used to optimize the graph. A "cone read" is a struct.get or
// array.get of a type that is not exact, so it can read the "cone" of all the
// subtypes. In general a read of a cone type (as opposed to an exact type) will
// require N incoming links, from each of the N subtypes - and we need that
// for each struct.get of a cone. If there are M such gets then we have N * M
// edges for this. Instead, we make a single canonical "cone read" location, and
// add a single link to it from each get, which is only N + M (plus the cost
// of adding "latency" in requiring an additional step along the way for the
// data to flow along).
struct ConeReadLocation {
HeapType type;
// The index of the field in a struct, or 0 for an array (where we do not
// attempt to differentiate by index).
Index index;
bool operator==(const ConeReadLocation& other) const {
return type == other.type && index == other.index;
}
};
// A location is a variant over all the possible flavors of locations that we
// have.
using Location = std::variant<ExpressionLocation,
ResultLocation,
LocalLocation,
BreakTargetLocation,
GlobalLocation,
SignatureParamLocation,
SignatureResultLocation,
DataLocation,
TagLocation,
NullLocation,
ConeReadLocation>;
} // namespace wasm
namespace std {
std::ostream& operator<<(std::ostream& stream,
const wasm::PossibleContents& contents);
template<> struct hash<wasm::PossibleContents> {
size_t operator()(const wasm::PossibleContents& contents) const {
return contents.hash();
}
};
// Define hashes of all the *Location flavors so that Location itself is
// hashable and we can use it in unordered maps and sets.
template<> struct hash<wasm::ExpressionLocation> {
size_t operator()(const wasm::ExpressionLocation& loc) const {
return std::hash<std::pair<size_t, wasm::Index>>{}(
{size_t(loc.expr), loc.tupleIndex});
}
};
template<> struct hash<wasm::ResultLocation> {
size_t operator()(const wasm::ResultLocation& loc) const {
return std::hash<std::pair<size_t, wasm::Index>>{}(
{size_t(loc.func), loc.index});
}
};
template<> struct hash<wasm::LocalLocation> {
size_t operator()(const wasm::LocalLocation& loc) const {
return std::hash<std::pair<size_t, std::pair<wasm::Index, wasm::Index>>>{}(
{size_t(loc.func), {loc.index, loc.tupleIndex}});
}
};
template<> struct hash<wasm::BreakTargetLocation> {
size_t operator()(const wasm::BreakTargetLocation& loc) const {
return std::hash<std::pair<size_t, std::pair<wasm::Name, wasm::Index>>>{}(
{size_t(loc.func), {loc.target, loc.tupleIndex}});
}
};
template<> struct hash<wasm::GlobalLocation> {
size_t operator()(const wasm::GlobalLocation& loc) const {
return std::hash<wasm::Name>{}(loc.name);
}
};
template<> struct hash<wasm::SignatureParamLocation> {
size_t operator()(const wasm::SignatureParamLocation& loc) const {
return std::hash<std::pair<wasm::HeapType, wasm::Index>>{}(
{loc.type, loc.index});
}
};
template<> struct hash<wasm::SignatureResultLocation> {
size_t operator()(const wasm::SignatureResultLocation& loc) const {
return std::hash<std::pair<wasm::HeapType, wasm::Index>>{}(
{loc.type, loc.index});
}
};
template<> struct hash<wasm::DataLocation> {
size_t operator()(const wasm::DataLocation& loc) const {
return std::hash<std::pair<wasm::HeapType, wasm::Index>>{}(
{loc.type, loc.index});
}
};
template<> struct hash<wasm::TagLocation> {
size_t operator()(const wasm::TagLocation& loc) const {
return std::hash<std::pair<wasm::Name, wasm::Index>>{}(
{loc.tag, loc.tupleIndex});
}
};
template<> struct hash<wasm::NullLocation> {
size_t operator()(const wasm::NullLocation& loc) const {
return std::hash<wasm::Type>{}(loc.type);
}
};
template<> struct hash<wasm::ConeReadLocation> {
size_t operator()(const wasm::ConeReadLocation& loc) const {
return std::hash<std::pair<wasm::HeapType, wasm::Index>>{}(
{loc.type, loc.index});
}
};
} // namespace std
namespace wasm {
// Analyze the entire wasm file to find which contents are possible in which
// locations. This assumes a closed world and starts from roots - newly created
// values - and propagates them to the locations they reach. After the
// analysis the user of this class can ask which contents are possible at any
// location.
//
// This focuses on useful information for the typical user of this API.
// Specifically, we find out:
//
// 1. What locations have no content reaching them at all. That means the code
// is unreachable. (Other passes may handle this, but ContentOracle does it
// for all things, so it might catch situations other passes do not cover;
// and, it takes no effort to support this here).
// 2. For all locations, we try to find when they must contain a constant value
// like i32(42) or ref.func(foo).
// 3. For locations that contain references, information about the subtypes
// possible there. For example, if something has wasm type anyref in the IR,
// we might find it must contain an exact type of something specific.
//
// Note that there is not much use in providing type info for locations that are
// *not* references. If a local is i32, for example, then it cannot contain any
// subtype anyhow, since i32 is not a reference and has no subtypes. And we know
// the type i32 from the wasm anyhow, that is, the caller will know it.
// Therefore the only useful information we can provide on top of the info
// already in the wasm is either that nothing can be there (1, above), or that a
// constant must be there (2, above), and so we do not make an effort to track
// non-reference types here. This makes the internals of ContentOracle simpler
// and faster. A noticeable outcome of that is that querying the contents of an
// i32 local will return Many and not ExactType{i32} (assuming we could not
// infer either that there must be nothing there, or a constant). Again, the
// caller is assumed to know the wasm IR type anyhow, and also other
// optimization passes work on the types in the IR, so we do not focus on that
// here.
class ContentOracle {
Module& wasm;
void analyze();
public:
ContentOracle(Module& wasm) : wasm(wasm) { analyze(); }
// Get the contents possible at a location.
PossibleContents getContents(Location location) {
auto iter = locationContents.find(location);
if (iter == locationContents.end()) {
// We know of no possible contents here.
return PossibleContents::none();
}
return iter->second;
}
private:
std::unordered_map<Location, PossibleContents> locationContents;
};
} // namespace wasm
#endif // wasm_ir_possible_contents_h