| /* | |
| * Copyright 2021 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_struct_utils_h | |
| #define wasm_ir_struct_utils_h | |
| #include "ir/properties.h" | |
| #include "ir/subtypes.h" | |
| #include "wasm-type.h" | |
| #include "wasm.h" | |
| namespace wasm { | |
| // A pair of a struct type and a field index, together defining a field in a | |
| // particular type. | |
| using StructField = std::pair<HeapType, Index>; | |
| namespace StructUtils { | |
| // A value that has a single bool, and implements combine() so it can be used in | |
| // StructValues. | |
| struct CombinableBool { | |
| bool value = false; | |
| CombinableBool() {} | |
| CombinableBool(bool value) : value(value) {} | |
| operator bool() const { return value; } | |
| bool combine(const CombinableBool& other) { | |
| if (!value && other.value) { | |
| value = true; | |
| return true; | |
| } | |
| return false; | |
| } | |
| }; | |
| static const Index DescriptorIndex = -1; | |
| // A vector of a template type's values. One such vector will be used per struct | |
| // type, where each element in the vector represents a field. We always assume | |
| // that the vectors are pre-initialized to the right length before accessing any | |
| // data, which this class enforces using assertions, and which is implemented in | |
| // StructValuesMap. | |
| template<typename T> struct StructValues : public std::vector<T> { | |
| T& operator[](size_t index) { | |
| if (index == DescriptorIndex) { | |
| return desc; | |
| } | |
| assert(index < this->size()); | |
| return std::vector<T>::operator[](index); | |
| } | |
| const T& operator[](size_t index) const { | |
| if (index == DescriptorIndex) { | |
| return desc; | |
| } | |
| assert(index < this->size()); | |
| return std::vector<T>::operator[](index); | |
| } | |
| // Store the descriptor as another field. (This could be a std::optional to | |
| // indicate that the descriptor's existence depends on the type, but that | |
| // would add overhead & code clutter (type checks). If there is no descriptor, | |
| // this will just hang around with the default values, not harming anything | |
| // except perhaps for looking a little odd during debugging. And whenever we | |
| // combine() a non-existent descriptor, we are doing unneeded work, but the | |
| // data here is typically just a few bools, so it is simpler and likely | |
| // faster to just copy those rather than check if the type has a descriptor.) | |
| T desc; | |
| }; | |
| // Maps heap types to a StructValues for that heap type. Includes exactness in | |
| // the key to allow differentiating between values for exact and inexact | |
| // references to each type. | |
| // | |
| // Also provides a combineInto() helper that combines one map into another. This | |
| // depends on the underlying T defining a combine() method. | |
| template<typename T> | |
| struct StructValuesMap | |
| : public std::unordered_map<std::pair<HeapType, Exactness>, StructValues<T>> { | |
| // When we access an item, if it does not already exist, create it with a | |
| // vector of the right length for that type. | |
| StructValues<T>& operator[](std::pair<HeapType, Exactness> type) { | |
| assert(type.first.isStruct()); | |
| auto inserted = this->insert({type, {}}); | |
| auto& values = inserted.first->second; | |
| if (inserted.second) { | |
| values.resize(type.first.getStruct().fields.size()); | |
| } | |
| return values; | |
| } | |
| void combineInto(StructValuesMap<T>& combinedInfos) const { | |
| for (auto& [type, info] : *this) { | |
| for (Index i = 0; i < info.size(); i++) { | |
| combinedInfos[type][i].combine(info[i]); | |
| } | |
| combinedInfos[type].desc.combine(info.desc); | |
| } | |
| } | |
| void dump(std::ostream& o) { | |
| o << "dump " << this << '\n'; | |
| for (auto& [type, vec] : (*this)) { | |
| o << "dump " << type.first << (type.second == Exact ? " exact " : " ") | |
| << &vec << ' '; | |
| for (auto x : vec) { | |
| x.dump(o); | |
| o << " "; | |
| }; | |
| o << " desc: "; | |
| vec.desc.dump(o); | |
| o << '\n'; | |
| } | |
| } | |
| }; | |
| // Map of functions to StructValuesMap. This lets us compute in parallel while | |
| // we walk the module, and afterwards we will merge them all. | |
| template<typename T> | |
| struct FunctionStructValuesMap | |
| : public std::unordered_map<Function*, StructValuesMap<T>> { | |
| FunctionStructValuesMap(Module& wasm) { | |
| // Initialize the data for each function in preparation for parallel | |
| // computation. | |
| for (auto& func : wasm.functions) { | |
| this->try_emplace(func.get()); | |
| } | |
| } | |
| // Combine information across functions. | |
| void combineInto(StructValuesMap<T>& combinedInfos) const { | |
| for (auto& kv : *this) { | |
| const StructValuesMap<T>& infos = kv.second; | |
| infos.combineInto(combinedInfos); | |
| } | |
| } | |
| }; | |
| // A generic scanner that finds struct operations and calls hooks to update | |
| // information. Subclasses must define these methods: | |
| // | |
| // * Note an expression written into a field. | |
| // | |
| // void noteExpression(Expression* expr, HeapType type, Index index, T& info); | |
| // | |
| // * Note a default value written during creation. | |
| // | |
| // void noteDefault(Type fieldType, HeapType type, Index index, T& info); | |
| // | |
| // * Note a RMW operation on a field. TODO: Pass more useful information here. | |
| // | |
| // void noteRMW(Expression* expr, HeapType type, Index index, T& info); | |
| // | |
| // * Note a copied value (read from a struct field and written to another struct | |
| // field). | |
| // | |
| // void noteCopy(StructGet* src, Type dstType, Index index, T& info); | |
| // | |
| // * Note a read. | |
| // | |
| // void noteRead(HeapType type, Index index, T& info); | |
| // | |
| // We track information from struct.new and struct.set/struct.get separately, | |
| // because in struct.new we know more about the type - we know the actual exact | |
| // type being written to, and not just that it is of a subtype of the | |
| // instruction's type, which helps later. | |
| // | |
| // Descriptors are treated as fields in that we call the above functions on | |
| // them. We pass DescriptorIndex for their index as a fake value. | |
| template<typename T, typename SubType> | |
| struct StructScanner : public WalkerPass<PostWalker<SubType>> { | |
| bool isFunctionParallel() override { return true; } | |
| bool modifiesBinaryenIR() override { return false; } | |
| SubType& self() { return *static_cast<SubType*>(this); } | |
| StructScanner(FunctionStructValuesMap<T>& functionNewInfos, | |
| FunctionStructValuesMap<T>& functionSetGetInfos) | |
| : functionNewInfos(functionNewInfos), | |
| functionSetGetInfos(functionSetGetInfos) {} | |
| void visitStructNew(StructNew* curr) { | |
| auto type = curr->type; | |
| if (type == Type::unreachable) { | |
| return; | |
| } | |
| // Note writes to all the fields of the struct. | |
| auto heapType = type.getHeapType(); | |
| auto& fields = heapType.getStruct().fields; | |
| auto ht = std::make_pair(heapType, Exact); | |
| auto& infos = functionNewInfos[this->getFunction()][ht]; | |
| for (Index i = 0; i < fields.size(); i++) { | |
| if (curr->isWithDefault()) { | |
| self().noteDefault(fields[i].type, heapType, i, infos[i]); | |
| } else { | |
| noteExpressionOrCopy(curr->operands[i], type, i, infos[i]); | |
| } | |
| } | |
| if (curr->desc) { | |
| self().noteExpression(curr->desc, heapType, DescriptorIndex, infos.desc); | |
| } | |
| } | |
| void visitStructSet(StructSet* curr) { | |
| auto type = curr->ref->type; | |
| if (type == Type::unreachable || type.isNull()) { | |
| return; | |
| } | |
| // Note a write to this field of the struct. | |
| auto ht = std::make_pair(type.getHeapType(), type.getExactness()); | |
| noteExpressionOrCopy( | |
| curr->value, | |
| type, | |
| curr->index, | |
| functionSetGetInfos[this->getFunction()][ht][curr->index]); | |
| } | |
| void visitStructGet(StructGet* curr) { | |
| auto type = curr->ref->type; | |
| if (type == Type::unreachable || type.isNull()) { | |
| return; | |
| } | |
| auto ht = std::make_pair(type.getHeapType(), type.getExactness()); | |
| auto index = curr->index; | |
| self().noteRead(type.getHeapType(), | |
| index, | |
| functionSetGetInfos[this->getFunction()][ht][index]); | |
| } | |
| void visitStructRMW(StructRMW* curr) { | |
| auto type = curr->ref->type; | |
| if (type == Type::unreachable || type.isNull()) { | |
| return; | |
| } | |
| auto heapType = type.getHeapType(); | |
| auto ht = std::make_pair(heapType, type.getExactness()); | |
| auto index = curr->index; | |
| auto& info = functionSetGetInfos[this->getFunction()][ht][index]; | |
| if (curr->op == RMWXchg) { | |
| // An xchg is really like a read and write combined. | |
| self().noteRead(heapType, index, info); | |
| noteExpressionOrCopy(curr->value, type, index, info); | |
| return; | |
| } | |
| // Otherwise we don't have a simple expression to describe the written | |
| // value, so fall back to noting an opaque RMW. | |
| self().noteRMW(curr->value, heapType, index, info); | |
| } | |
| void visitStructCmpxchg(StructCmpxchg* curr) { | |
| auto type = curr->ref->type; | |
| if (type == Type::unreachable || type.isNull()) { | |
| return; | |
| } | |
| auto heapType = type.getHeapType(); | |
| auto ht = std::make_pair(heapType, type.getExactness()); | |
| auto index = curr->index; | |
| auto& info = functionSetGetInfos[this->getFunction()][ht][index]; | |
| // A cmpxchg is like a read and conditional write. | |
| self().noteRead(heapType, index, info); | |
| noteExpressionOrCopy(curr->replacement, type, index, info); | |
| } | |
| void visitRefCast(RefCast* curr) { | |
| if (curr->desc) { | |
| // We may try to read a descriptor from anything arriving in |curr->ref|, | |
| // but the only things that matter are the things we cast to: other types | |
| // can lack a descriptor, and are skipped anyhow. So the only effective | |
| // read is of the cast type. | |
| handleDescRead(curr->getCastType()); | |
| } | |
| } | |
| void visitBrOn(BrOn* curr) { | |
| if (curr->desc && | |
| (curr->op == BrOnCastDescEq || curr->op == BrOnCastDescEqFail)) { | |
| handleDescRead(curr->getCastType()); | |
| } | |
| } | |
| void visitRefGetDesc(RefGetDesc* curr) { | |
| // Unlike a cast, anything in |curr->ref| may be read from. | |
| handleDescRead(curr->ref->type); | |
| } | |
| void handleDescRead(Type type) { | |
| if (type == Type::unreachable) { | |
| return; | |
| } | |
| auto heapType = type.getHeapType(); | |
| auto ht = std::make_pair(heapType, type.getExactness()); | |
| if (heapType.isStruct()) { | |
| // Any subtype of the reference here may be read from. | |
| self().noteRead(heapType, | |
| DescriptorIndex, | |
| functionSetGetInfos[this->getFunction()][ht].desc); | |
| return; | |
| } | |
| } | |
| void noteExpressionOrCopy(Expression* expr, Type type, Index index, T& info) { | |
| auto* fallthrough = | |
| Properties::getFallthrough(expr, | |
| this->getPassOptions(), | |
| *this->getModule(), | |
| self().getFallthroughBehavior()); | |
| // TODO: Consider lifting this restriction on the use of fallthrough values. | |
| if (fallthrough->type == expr->type) { | |
| expr = fallthrough; | |
| } | |
| if (auto* get = expr->dynCast<StructGet>(); | |
| get && get->ref->type.isStruct()) { | |
| self().noteCopy(get, type, index, info); | |
| return; | |
| } | |
| self().noteExpression(expr, type.getHeapType(), index, info); | |
| } | |
| Properties::FallthroughBehavior getFallthroughBehavior() { | |
| // By default, look at and use tee&br_if fallthrough values. | |
| return Properties::FallthroughBehavior::AllowTeeBrIf; | |
| } | |
| FunctionStructValuesMap<T>& functionNewInfos; | |
| FunctionStructValuesMap<T>& functionSetGetInfos; | |
| }; | |
| // Helper class to propagate information to sub- and/or super- classes in the | |
| // type hierarchy. While propagating it calls a method | |
| // | |
| // to.combine(from) | |
| // | |
| // which combines the information from |from| into |to|, and should return true | |
| // if we changed something. | |
| template<typename T> class TypeHierarchyPropagator { | |
| public: | |
| // Constructor that gets a module and computes subtypes. | |
| TypeHierarchyPropagator(Module& wasm) : subTypes(wasm) {} | |
| // Constructor that gets subtypes and uses them, avoiding a scan of a | |
| // module. TODO: avoid a copy here? | |
| TypeHierarchyPropagator(const SubTypes& subTypes) : subTypes(subTypes) {} | |
| SubTypes subTypes; | |
| // Propagate given a StructValuesMap, which means we need to take into | |
| // account fields. | |
| void propagateToSuperTypes(StructValuesMap<T>& infos) { | |
| propagate(infos, false, true); | |
| } | |
| void propagateToSubTypes(StructValuesMap<T>& infos) { | |
| propagate(infos, true, false); | |
| } | |
| void propagateToSuperAndSubTypes(StructValuesMap<T>& infos) { | |
| propagate(infos, true, true); | |
| } | |
| // Propagate on a simpler map of structs and infos (that is, not using | |
| // separate values for the fields, as StructValuesMap does). This is useful | |
| // when not tracking individual fields, but something more general about | |
| // types. | |
| using StructMap = std::unordered_map<HeapType, T>; | |
| void propagateToSuperTypes(StructMap& infos) { | |
| propagate(infos, false, true); | |
| } | |
| void propagateToSubTypes(StructMap& infos) { propagate(infos, true, false); } | |
| void propagateToSuperAndSubTypes(StructMap& infos) { | |
| propagate(infos, true, true); | |
| } | |
| private: | |
| void propagate(StructValuesMap<T>& combinedInfos, | |
| bool toSubTypes, | |
| bool toSuperTypes) { | |
| UniqueDeferredQueue<std::pair<HeapType, Exactness>> work; | |
| for (auto& [ht, _] : combinedInfos) { | |
| work.push(ht); | |
| } | |
| while (!work.empty()) { | |
| auto [type, exactness] = work.pop(); | |
| auto& infos = combinedInfos[{type, exactness}]; | |
| if (toSuperTypes) { | |
| // Propagate shared fields to the supertype, which may be the inexact | |
| // version of the same type. | |
| std::optional<std::pair<HeapType, Exactness>> super; | |
| if (exactness == Exact) { | |
| super = {type, Inexact}; | |
| } else if (auto superType = type.getDeclaredSuperType()) { | |
| super = {*superType, Inexact}; | |
| } | |
| if (super) { | |
| auto& superInfos = combinedInfos[*super]; | |
| const auto& superFields = &super->first.getStruct().fields; | |
| for (Index i = 0; i < superFields->size(); i++) { | |
| if (superInfos[i].combine(infos[i])) { | |
| work.push(*super); | |
| } | |
| } | |
| // Propagate the descriptor to the super, if the super has one. | |
| if (super->first.getDescriptorType() && | |
| superInfos.desc.combine(infos.desc)) { | |
| work.push(*super); | |
| } | |
| } | |
| } | |
| if (toSubTypes && exactness == Inexact) { | |
| // Propagate shared fields to the subtypes, which may just be the exact | |
| // version of the same type. | |
| auto numFields = type.getStruct().fields.size(); | |
| auto handleSubtype = [&](std::pair<HeapType, Exactness> sub) { | |
| auto& subInfos = combinedInfos[sub]; | |
| for (Index i = 0; i < numFields; i++) { | |
| if (subInfos[i].combine(infos[i])) { | |
| work.push(sub); | |
| } | |
| } | |
| // Propagate the descriptor. | |
| if (subInfos.desc.combine(infos.desc)) { | |
| work.push(sub); | |
| } | |
| }; | |
| handleSubtype({type, Exact}); | |
| for (auto subType : subTypes.getImmediateSubTypes(type)) { | |
| handleSubtype({subType, Inexact}); | |
| } | |
| } | |
| } | |
| } | |
| void propagate(StructMap& combinedInfos, bool toSubTypes, bool toSuperTypes) { | |
| UniqueDeferredQueue<HeapType> work; | |
| for (auto& [type, _] : combinedInfos) { | |
| work.push(type); | |
| } | |
| while (!work.empty()) { | |
| auto type = work.pop(); | |
| auto& info = combinedInfos[type]; | |
| if (toSuperTypes) { | |
| // Propagate to the supertype. | |
| if (auto superType = type.getDeclaredSuperType()) { | |
| auto& superInfo = combinedInfos[*superType]; | |
| if (superInfo.combine(info)) { | |
| work.push(*superType); | |
| } | |
| } | |
| } | |
| if (toSubTypes) { | |
| // Propagate shared fields to the subtypes. | |
| for (auto subType : subTypes.getImmediateSubTypes(type)) { | |
| auto& subInfo = combinedInfos[subType]; | |
| if (subInfo.combine(info)) { | |
| work.push(subType); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| } // namespace StructUtils | |
| } // namespace wasm | |
| #endif // wasm_ir_struct_utils_h |