| /* | |
| * 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. | |
| */ | |
| #ifndef wasm_ir_effects_h | |
| #define wasm_ir_effects_h | |
| #include <cassert> | |
| #include <unordered_set> | |
| #include "ir/intrinsics.h" | |
| #include "pass.h" | |
| #include "support/name.h" | |
| #include "support/utilities.h" | |
| #include "wasm-traversal.h" | |
| #include "wasm-type.h" | |
| #include "wasm.h" | |
| namespace wasm { | |
| // Analyze various possible effects. | |
| class EffectAnalyzer { | |
| public: | |
| EffectAnalyzer(const PassOptions& passOptions, const Module& module) | |
| : ignoreImplicitTraps(passOptions.ignoreImplicitTraps), | |
| trapsNeverHappen(passOptions.trapsNeverHappen), branchesOut(false), | |
| calls(false), readsMemory(false), writesMemory(false), | |
| readsSharedMemory(false), writesSharedMemory(false), readsTable(false), | |
| writesTable(false), readsMutableStruct(false), writesStruct(false), | |
| readsSharedMutableStruct(false), writesSharedStruct(false), | |
| readsMutableArray(false), writesArray(false), | |
| readsSharedMutableArray(false), writesSharedArray(false), trap(false), | |
| implicitTrap(false), throws_(false), danglingPop(false), | |
| mayNotReturn(false), hasReturnCallThrow(false), module(module), | |
| features(module.features) {} | |
| EffectAnalyzer(const PassOptions& passOptions, | |
| const Module& module, | |
| Expression* ast) | |
| : EffectAnalyzer(passOptions, module) { | |
| walk(ast); | |
| } | |
| EffectAnalyzer(const PassOptions& passOptions, Module& module, Function* func) | |
| : EffectAnalyzer(passOptions, module) { | |
| walk(func); | |
| } | |
| bool ignoreImplicitTraps : 1; | |
| bool trapsNeverHappen : 1; | |
| // Definitely branches out of this expression, or does a return, etc. | |
| // breakTargets tracks individual targets, which we may eventually see are | |
| // internal, while this is set when we see something that will definitely | |
| // not be internal, or is otherwise special like an infinite loop (which | |
| // does not technically branch "out", but it does break the normal assumption | |
| // of control flow proceeding normally). | |
| bool branchesOut : 1; | |
| bool calls : 1; | |
| bool readsMemory : 1; | |
| bool writesMemory : 1; | |
| bool readsSharedMemory : 1; | |
| bool writesSharedMemory : 1; | |
| bool readsTable : 1; | |
| bool writesTable : 1; | |
| // TODO: More specific type-based alias analysis, and not just at the | |
| // struct/array level. | |
| bool readsMutableStruct : 1; | |
| bool writesStruct : 1; | |
| bool readsSharedMutableStruct : 1; | |
| bool writesSharedStruct : 1; | |
| bool readsMutableArray : 1; | |
| bool writesArray : 1; | |
| bool readsSharedMutableArray : 1; | |
| bool writesSharedArray : 1; | |
| // A trap, either from an unreachable instruction, or from an implicit trap | |
| // that we do not ignore (see below). | |
| // | |
| // Note that we ignore trap differences, so it is ok to reorder traps with | |
| // each other, but it is not ok to remove them or reorder them with other | |
| // effects in a noticeable way. | |
| // | |
| // Note also that we ignore *optional* runtime-specific traps: we only | |
| // consider as trapping something that will trap in *all* VMs, and *all* the | |
| // time. For example, a single allocation might trap in a VM in a particular | |
| // execution, if it happens to run out of memory just there, but that is not | |
| // enough for us to mark it as having a trap effect. (Note that not marking | |
| // each allocation as possibly trapping has the nice benefit of making it | |
| // possible to eliminate an allocation whose result is not captured.) OTOH, we | |
| // *do* mark a potentially infinite number of allocations as trapping, as all | |
| // VMs would trap eventually, and the same for potentially infinite recursion, | |
| // etc. | |
| bool trap : 1; | |
| // A trap from an instruction like a load or div/rem, which may trap on corner | |
| // cases. If we do not ignore implicit traps then these are counted as a trap. | |
| bool implicitTrap : 1; | |
| bool throws_ : 1; | |
| // If this expression contains 'pop's that are not enclosed in 'catch' body. | |
| // For example, (drop (pop i32)) should set this to true. | |
| bool danglingPop : 1; | |
| // Whether this code may "hang" and not eventually complete. An infinite loop, | |
| // or a continuation that is never continued, are examples of that. | |
| bool mayNotReturn : 1; | |
| // Since return calls return out of the body of the function before performing | |
| // their call, they are indistinguishable from normal returns from the | |
| // perspective of their surrounding code, and the return-callee's effects only | |
| // become visible when considering the effects of the whole function | |
| // containing the return call. To model this correctly, stash the callee's | |
| // effects on the side and only merge them in after walking a full function | |
| // body. | |
| // | |
| // We currently do this stashing only for the throw effect, but in principle | |
| // we could do it for all effects if it made a difference. (Only throw is | |
| // noticeable now because the only thing that can change between doing the | |
| // call here and doing it outside at the function exit is the scoping of | |
| // try-catch blocks. If future wasm scoping additions are added, we may need | |
| // more here.) | |
| bool hasReturnCallThrow : 1; | |
| const Module& module; | |
| FeatureSet features; | |
| std::set<Index> localsRead; | |
| std::set<Index> localsWritten; | |
| std::unordered_set<Name> mutableGlobalsRead; | |
| std::unordered_set<Name> globalsWritten; | |
| // The nested depth of try-catch_all. If an instruction that may throw is | |
| // inside an inner try-catch_all, we don't mark it as 'throws_', because it | |
| // will be caught by an inner catch_all. We only count 'try's with a | |
| // 'catch_all' because instructions within a 'try' without a 'catch_all' can | |
| // still throw outside of the try. | |
| size_t tryDepth = 0; | |
| // The nested depth of catch. This is necessary to track danglng pops. | |
| size_t catchDepth = 0; | |
| // The strongest memory orders used to read and write to any shared location | |
| // (e.g. shared memories, shared GC structs, etc), if any. | |
| MemoryOrder readOrder = MemoryOrder::Unordered; | |
| MemoryOrder writeOrder = MemoryOrder::Unordered; | |
| // Walk an expression and all its children. | |
| void walk(Expression* ast) { | |
| InternalAnalyzer(*this).walk(ast); | |
| post(); | |
| } | |
| // Visit an expression, without any children. | |
| void visit(Expression* ast) { | |
| InternalAnalyzer(*this).visit(ast); | |
| post(); | |
| } | |
| // Walk an entire function body. This will ignore effects that are not | |
| // noticeable from the perspective of the caller, that is, effects that are | |
| // only noticeable during the call, but "vanish" when the call stack is | |
| // unwound. | |
| // | |
| // Unlike walking just the body, walking the function will also | |
| // include the effects of any return calls the function makes. For that | |
| // reason, it is a bug if a user of this code calls walk(Expression*) and not | |
| // walk(Function*) if their intention is to scan an entire function body. | |
| // Putting it another way, a return_call is syntax sugar for a return and a | |
| // call, where the call executes at the function scope, so there is a | |
| // meaningful difference between scanning an expression and scanning | |
| // the entire function body. | |
| void walk(Function* func) { | |
| walk(func->body); | |
| // Effects of return-called functions will be visible to the caller. | |
| if (hasReturnCallThrow) { | |
| throws_ = true; | |
| } | |
| // We can ignore branching out of the function body - this can only be | |
| // a return, and that is only noticeable in the function, not outside. | |
| branchesOut = false; | |
| // When the function exits, changes to locals cannot be noticed any more. | |
| localsWritten.clear(); | |
| localsRead.clear(); | |
| } | |
| // Helper functions to check for various effect types | |
| bool accessesLocal() const { | |
| return localsRead.size() + localsWritten.size() > 0; | |
| } | |
| bool accessesMutableGlobal() const { | |
| return globalsWritten.size() + mutableGlobalsRead.size() > 0; | |
| } | |
| bool accessesMemory() const { return calls || readsMemory || writesMemory; } | |
| bool accessesSharedMemory() const { | |
| return calls || readsSharedMemory || writesSharedMemory; | |
| } | |
| bool accessesTable() const { return calls || readsTable || writesTable; } | |
| bool accessesMutableStruct() const { | |
| return calls || readsMutableStruct || writesStruct; | |
| } | |
| bool accessesSharedMutableStruct() const { | |
| return calls || readsSharedMutableStruct || writesSharedStruct; | |
| } | |
| bool accessesArray() const { | |
| return calls || readsMutableArray || writesArray; | |
| } | |
| bool accessesSharedArray() const { | |
| return calls || readsSharedMutableArray || writesSharedArray; | |
| } | |
| bool throws() const { return throws_ || !delegateTargets.empty(); } | |
| // Check whether this may transfer control flow to somewhere outside of this | |
| // expression (aside from just flowing out normally). That includes a break | |
| // or a throw (if the throw is not known to be caught inside this expression; | |
| // note that if the throw is not caught in this expression then it might be | |
| // caught in this function but outside of this expression, or it might not be | |
| // caught in the function at all, which would mean control flow cannot be | |
| // transferred inside the function, but this expression does not know that). | |
| bool transfersControlFlow() const { | |
| return branchesOut || throws() || hasExternalBreakTargets(); | |
| } | |
| // Changes something in globally-stored state. | |
| bool writesGlobalState() const { | |
| return globalsWritten.size() || writesMemory || writesSharedMemory || | |
| writesTable || writesStruct || writesSharedStruct || writesArray || | |
| writesSharedArray || calls; | |
| } | |
| bool readsMutableGlobalState() const { | |
| return mutableGlobalsRead.size() || readsMemory || readsSharedMemory || | |
| readsTable || readsMutableStruct || readsSharedMutableStruct || | |
| readsMutableArray || readsSharedMutableArray || calls; | |
| } | |
| bool accessesSharedGlobalState() const { | |
| return readsSharedMemory || writesSharedMemory || | |
| readsSharedMutableStruct || writesSharedStruct || | |
| readsSharedMutableArray || writesSharedArray || calls; | |
| } | |
| bool hasSynchronization() const { | |
| return readOrder != MemoryOrder::Unordered || | |
| writeOrder != MemoryOrder::Unordered; | |
| } | |
| bool hasNonTrapSideEffects() const { | |
| return localsWritten.size() > 0 || danglingPop || writesGlobalState() || | |
| throws() || transfersControlFlow() || hasSynchronization() || | |
| mayNotReturn; | |
| } | |
| bool hasSideEffects() const { return trap || hasNonTrapSideEffects(); } | |
| // Check if there are side effects, and they are of a kind that cannot be | |
| // removed by optimization passes. | |
| // | |
| // The difference between this and hasSideEffects is subtle, and only related | |
| // to trapsNeverHappen - if trapsNeverHappen then any trap we see is removable | |
| // by optimizations. In general, you should call hasSideEffects, and only call | |
| // this method if you are certain that it is a place that would not perform an | |
| // unsafe transformation with a trap. Specifically, if a pass calls this | |
| // and gets the result that there are no unremovable side effects, then it | |
| // must either | |
| // | |
| // 1. Remove any side effects present, if any, so they no longer exist. | |
| // 2. Keep the code exactly where it is. | |
| // | |
| // If instead of 1&2 a pass kept the side effect and also reordered the code | |
| // with other things, then that could be bad, as the side effect might have | |
| // been behind a condition that avoids it occurring. | |
| // | |
| // TODO: Go through the optimizer and use this in all places that do not move | |
| // code around. | |
| bool hasUnremovableSideEffects() const { | |
| return hasNonTrapSideEffects() || (trap && !trapsNeverHappen); | |
| } | |
| bool hasAnything() const { | |
| return hasSideEffects() || accessesLocal() || readsMutableGlobalState(); | |
| } | |
| // check if we break to anything external from ourselves | |
| bool hasExternalBreakTargets() const { return !breakTargets.empty(); } | |
| // Checks if these effects must remain ordered before another set of effects | |
| // (e.g., if we write, we must remain ordered before someone that reads). | |
| // | |
| // This assumes the things whose effects we are comparing will both execute, | |
| // at least if neither of them transfers control flow away. We assume there is | |
| // no transfer of control flow *between* them. It is not valid to call this | |
| // method in other situations, like this: | |
| // | |
| // A | |
| // (br_if 0 (local.get 0)) ;; this may transfer control flow away | |
| // B | |
| // | |
| // Calling this method in that situation is invalid because only A may | |
| // execute and not B. The following are examples of situations where it is | |
| // valid to call this method: | |
| // | |
| // A | |
| // ;; nothing in between them at all | |
| // B | |
| // | |
| // A | |
| // (local.set 0 (i32.const 0)) ;; something in between without a possible | |
| // ;; control flow transfer | |
| // B | |
| // | |
| // (Note that if they appear in inside a loop, A and B may overlap or even be | |
| // the same expression; this is fine because A still executes before B, even | |
| // if also executes during and after B across different loop iterations.) That | |
| // A and B both execute only matters in the case of traps-never-happen: in | |
| // that mode we can move traps but only if doing so would not make them start | |
| // to appear when they did not previously. In the example with the br_if we | |
| // can't reorder A and B if B traps, but in the valid examples we can reorder | |
| // them even if B traps (even if A has a global effect like a global.set, | |
| // since we assume B does not trap in traps-never-happen). | |
| bool orderedBefore(const EffectAnalyzer& other) const { | |
| // Cannot reorder control flow and side effects. | |
| if ((transfersControlFlow() && other.hasSideEffects()) || | |
| (other.transfersControlFlow() && hasSideEffects())) { | |
| return true; | |
| } | |
| // write-write, write-read, and read-write conflicts on possibly aliasing | |
| // locations prevent reordering. Calls may access these locations. | |
| if (((writesMemory || calls) && other.accessesMemory()) || | |
| ((other.writesMemory || other.calls) && accessesMemory()) || | |
| ((writesSharedMemory || calls) && other.accessesSharedMemory()) || | |
| ((other.writesSharedMemory || other.calls) && accessesSharedMemory()) || | |
| ((writesTable || calls) && other.accessesTable()) || | |
| ((other.writesTable || other.calls) && accessesTable()) || | |
| ((writesStruct || calls) && other.accessesMutableStruct()) || | |
| ((other.writesStruct || other.calls) && accessesMutableStruct()) || | |
| ((writesSharedStruct || calls) && | |
| other.accessesSharedMutableStruct()) || | |
| ((other.writesSharedStruct || other.calls) && | |
| accessesSharedMutableStruct()) || | |
| ((writesArray || calls) && other.accessesArray()) || | |
| ((other.writesArray || other.calls) && accessesArray()) || | |
| ((writesSharedArray || calls) && other.accessesSharedArray()) || | |
| ((other.writesSharedArray || other.calls) && accessesSharedArray())) { | |
| return true; | |
| } | |
| // Cannot reorder anything before dangling pops. | |
| if (danglingPop) { | |
| return true; | |
| } | |
| // Shared location accesses cannot be reordered after (but may be able to be | |
| // reordered before) release stores. | |
| if (other.writeOrder >= MemoryOrder::AcqRel && | |
| accessesSharedGlobalState()) { | |
| return true; | |
| } | |
| // Shared location accesses cannot be reordered before (but may be able to | |
| // be reordered after) acquire loads. | |
| if (readOrder >= MemoryOrder::AcqRel && other.accessesSharedGlobalState()) { | |
| return true; | |
| } | |
| // No shared location accesses may be reordered in either direction around a | |
| // seqcst operation. | |
| if (((readOrder == MemoryOrder::SeqCst || | |
| writeOrder == MemoryOrder::SeqCst) && | |
| other.accessesSharedGlobalState()) || | |
| ((other.readOrder == MemoryOrder::SeqCst || | |
| other.writeOrder == MemoryOrder::SeqCst) && | |
| accessesSharedGlobalState())) { | |
| return true; | |
| } | |
| // write-write, write-read, and read-write conflicts on a local prevent | |
| // reordering. | |
| for (auto local : localsWritten) { | |
| if (other.localsRead.contains(local) || | |
| other.localsWritten.contains(local)) { | |
| return true; | |
| } | |
| } | |
| for (auto local : localsRead) { | |
| if (other.localsWritten.contains(local)) { | |
| return true; | |
| } | |
| } | |
| // write-write, write-read, and read-write conflicts on globals prevent | |
| // reordering. Unlike for locals, calls can access globals. | |
| if ((other.calls && accessesMutableGlobal()) || | |
| (calls && other.accessesMutableGlobal())) { | |
| return true; | |
| } | |
| for (auto global : globalsWritten) { | |
| if (other.mutableGlobalsRead.contains(global) || | |
| other.globalsWritten.contains(global)) { | |
| return true; | |
| } | |
| } | |
| for (auto global : mutableGlobalsRead) { | |
| if (other.globalsWritten.contains(global)) { | |
| return true; | |
| } | |
| } | |
| // Note that the above includes disallowing the reordering of a trap with an | |
| // exception (as an exception can transfer control flow inside the current | |
| // function, so transfersControlFlow would be true) - while we allow the | |
| // reordering of traps with each other, we do not reorder exceptions with | |
| // anything. | |
| assert(!((trap && other.throws()) || (throws() && other.trap))); | |
| // We can't reorder an implicit trap in a way that could alter what global | |
| // state is modified. However, in trapsNeverHappen mode we assume traps do | |
| // not occur in practice, which lets us ignore this, at least in the case | |
| // that the code executes. As mentioned above, we assume that there is no | |
| // transfer of control flow between the things we are comparing, so all we | |
| // need to do is check for such transfers in them. | |
| if (!trapsNeverHappen || transfersControlFlow() || | |
| other.transfersControlFlow()) { | |
| if ((trap && other.writesGlobalState()) || | |
| (other.trap && writesGlobalState())) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool orderedAfter(const EffectAnalyzer& other) { | |
| return other.orderedBefore(*this); | |
| } | |
| // Whether these effects are prevented from moving past the other effects in | |
| // either direction. | |
| // TODO: Update users to check order more precisely and remove this. | |
| bool invalidates(const EffectAnalyzer& other) { | |
| return orderedBefore(other) || orderedAfter(other); | |
| } | |
| void mergeIn(const EffectAnalyzer& other) { | |
| branchesOut = branchesOut || other.branchesOut; | |
| calls = calls || other.calls; | |
| readsMemory = readsMemory || other.readsMemory; | |
| writesMemory = writesMemory || other.writesMemory; | |
| readsSharedMemory = readsSharedMemory || other.readsSharedMemory; | |
| writesSharedMemory = writesSharedMemory || other.writesSharedMemory; | |
| readsTable = readsTable || other.readsTable; | |
| writesTable = writesTable || other.writesTable; | |
| readsMutableStruct = readsMutableStruct || other.readsMutableStruct; | |
| writesStruct = writesStruct || other.writesStruct; | |
| readsSharedMutableStruct = | |
| readsSharedMutableStruct || other.readsSharedMutableStruct; | |
| writesSharedStruct = writesSharedStruct || other.writesSharedStruct; | |
| readsMutableArray = readsMutableArray || other.readsMutableArray; | |
| writesArray = writesArray || other.writesArray; | |
| readsSharedMutableArray = | |
| readsSharedMutableArray || other.readsSharedMutableArray; | |
| writesSharedArray = writesSharedArray || other.writesSharedArray; | |
| trap = trap || other.trap; | |
| implicitTrap = implicitTrap || other.implicitTrap; | |
| trapsNeverHappen = trapsNeverHappen || other.trapsNeverHappen; | |
| throws_ = throws_ || other.throws_; | |
| danglingPop = danglingPop || other.danglingPop; | |
| mayNotReturn = mayNotReturn || other.mayNotReturn; | |
| hasReturnCallThrow = hasReturnCallThrow || other.hasReturnCallThrow; | |
| readOrder = std::max(readOrder, other.readOrder); | |
| writeOrder = std::max(writeOrder, other.writeOrder); | |
| for (auto i : other.localsRead) { | |
| localsRead.insert(i); | |
| } | |
| for (auto i : other.localsWritten) { | |
| localsWritten.insert(i); | |
| } | |
| for (auto i : other.mutableGlobalsRead) { | |
| mutableGlobalsRead.insert(i); | |
| } | |
| for (auto i : other.globalsWritten) { | |
| globalsWritten.insert(i); | |
| } | |
| for (auto i : other.breakTargets) { | |
| breakTargets.insert(i); | |
| } | |
| for (auto i : other.delegateTargets) { | |
| delegateTargets.insert(i); | |
| } | |
| } | |
| // the checks above happen after the node's children were processed, in the | |
| // order of execution we must also check for control flow that happens before | |
| // the children, i.e., loops | |
| bool checkPre(Expression* curr) { | |
| if (curr->is<Loop>()) { | |
| branchesOut = true; | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool checkPost(Expression* curr) { | |
| visit(curr); | |
| if (curr->is<Loop>()) { | |
| branchesOut = true; | |
| } | |
| return hasAnything(); | |
| } | |
| std::unordered_set<Name> breakTargets; | |
| std::unordered_set<Name> delegateTargets; | |
| private: | |
| struct InternalAnalyzer | |
| : public PostWalker<InternalAnalyzer, OverriddenVisitor<InternalAnalyzer>> { | |
| EffectAnalyzer& parent; | |
| InternalAnalyzer(EffectAnalyzer& parent) : parent(parent) {} | |
| static void scan(InternalAnalyzer* self, Expression** currp) { | |
| Expression* curr = *currp; | |
| // We need to decrement try depth before catch starts, so handle it | |
| // separately | |
| if (curr->is<Try>()) { | |
| self->pushTask(doVisitTry, currp); | |
| self->pushTask(doEndCatch, currp); | |
| auto& catchBodies = curr->cast<Try>()->catchBodies; | |
| for (int i = int(catchBodies.size()) - 1; i >= 0; i--) { | |
| self->pushTask(scan, &catchBodies[i]); | |
| } | |
| self->pushTask(doStartCatch, currp); | |
| self->pushTask(scan, &curr->cast<Try>()->body); | |
| self->pushTask(doStartTry, currp); | |
| return; | |
| } | |
| if (auto* tryTable = curr->dynCast<TryTable>()) { | |
| // We need to increment try depth before starting. | |
| self->pushTask(doEndTryTable, currp); | |
| self->pushTask(doVisitTryTable, currp); | |
| self->pushTask(scan, &tryTable->body); | |
| self->pushTask(doStartTryTable, currp); | |
| return; | |
| } | |
| PostWalker<InternalAnalyzer, OverriddenVisitor<InternalAnalyzer>>::scan( | |
| self, currp); | |
| } | |
| static void doStartTry(InternalAnalyzer* self, Expression** currp) { | |
| Try* curr = (*currp)->cast<Try>(); | |
| // We only count 'try's with a 'catch_all' because instructions within a | |
| // 'try' without a 'catch_all' can still throw outside of the try. | |
| if (curr->hasCatchAll()) { | |
| self->parent.tryDepth++; | |
| } | |
| } | |
| static void doStartCatch(InternalAnalyzer* self, Expression** currp) { | |
| Try* curr = (*currp)->cast<Try>(); | |
| // This is conservative. When an inner try-delegate targets the current | |
| // expression, even if the try-delegate's body can't throw, we consider | |
| // the current expression can throw for simplicity, unless the current | |
| // expression is not inside a try-catch_all. It is hard to figure out | |
| // whether the original try-delegate's body throws or not at this point. | |
| if (curr->name.is()) { | |
| if (self->parent.delegateTargets.contains(curr->name) && | |
| self->parent.tryDepth == 0) { | |
| self->parent.throws_ = true; | |
| } | |
| self->parent.delegateTargets.erase(curr->name); | |
| } | |
| // We only count 'try's with a 'catch_all' because instructions within a | |
| // 'try' without a 'catch_all' can still throw outside of the try. | |
| if (curr->hasCatchAll()) { | |
| assert(self->parent.tryDepth > 0 && "try depth cannot be negative"); | |
| self->parent.tryDepth--; | |
| } | |
| self->parent.catchDepth++; | |
| } | |
| static void doEndCatch(InternalAnalyzer* self, Expression** currp) { | |
| assert(self->parent.catchDepth > 0 && "catch depth cannot be negative"); | |
| self->parent.catchDepth--; | |
| } | |
| static void doStartTryTable(InternalAnalyzer* self, Expression** currp) { | |
| auto* curr = (*currp)->cast<TryTable>(); | |
| // We only count 'try_table's with a 'catch_all' because instructions | |
| // within a 'try_table' without a 'catch_all' can still throw outside of | |
| // the try. | |
| if (curr->hasCatchAll()) { | |
| self->parent.tryDepth++; | |
| } | |
| } | |
| static void doEndTryTable(InternalAnalyzer* self, Expression** currp) { | |
| auto* curr = (*currp)->cast<TryTable>(); | |
| if (curr->hasCatchAll()) { | |
| assert(self->parent.tryDepth > 0 && "try depth cannot be negative"); | |
| self->parent.tryDepth--; | |
| } | |
| } | |
| void readsMemory(Name memory, MemoryOrder order) { | |
| if (parent.module.getMemory(memory)->shared) { | |
| parent.readsSharedMemory = true; | |
| parent.readOrder = std::max(parent.readOrder, order); | |
| } else { | |
| parent.readsMemory = true; | |
| } | |
| } | |
| void writesMemory(Name memory, MemoryOrder order) { | |
| if (parent.module.getMemory(memory)->shared) { | |
| parent.writesSharedMemory = true; | |
| parent.writeOrder = std::max(parent.writeOrder, order); | |
| } else { | |
| parent.writesMemory = true; | |
| } | |
| } | |
| void readsStruct(HeapType type, Index index, MemoryOrder order) { | |
| assert(type.isStruct()); | |
| if (type.getStruct().fields[index].mutable_ == Mutable) { | |
| if (type.isShared()) { | |
| parent.readsSharedMutableStruct = true; | |
| parent.readOrder = std::max(parent.readOrder, order); | |
| } else { | |
| parent.readsMutableStruct = true; | |
| } | |
| } | |
| } | |
| void writesStruct(HeapType type, Index index, MemoryOrder order) { | |
| assert(type.isStruct()); | |
| if (type.isShared()) { | |
| parent.writesSharedStruct = true; | |
| parent.writeOrder = std::max(parent.writeOrder, order); | |
| } else { | |
| parent.writesStruct = true; | |
| } | |
| } | |
| void readsArray(HeapType type, MemoryOrder order) { | |
| assert(type.isArray()); | |
| if (type.getArray().element.mutable_ == Mutable) { | |
| if (type.isShared()) { | |
| parent.readsSharedMutableArray = true; | |
| parent.readOrder = std::max(parent.readOrder, order); | |
| } else { | |
| parent.readsMutableArray = true; | |
| } | |
| } | |
| } | |
| void writesArray(HeapType type, MemoryOrder order) { | |
| assert(type.isArray()); | |
| if (type.isShared()) { | |
| parent.writesSharedArray = true; | |
| parent.writeOrder = std::max(parent.writeOrder, order); | |
| } else { | |
| parent.writesArray = true; | |
| } | |
| } | |
| // Handle effects due to a null type arriving in a place where a null input | |
| // causes trapping. That is, handle the case of the type proving that the | |
| // input is null. | |
| // Returns true iff there is no need to consider further effects. | |
| bool trapOnNull(Type type) { | |
| if (type == Type::unreachable) { | |
| return true; | |
| } | |
| assert(type.isRef()); | |
| if (type.isNull()) { | |
| parent.trap = true; | |
| return true; | |
| } | |
| if (type.isNullable()) { | |
| parent.implicitTrap = true; | |
| } | |
| return false; | |
| } | |
| // Handle effects due to an explicit null check of the operands in `exprs`. | |
| // Returns true iff there is no need to consider further effects. | |
| bool trapOnNull(std::initializer_list<Expression*> exprs) { | |
| for (auto* expr : exprs) { | |
| if (expr && trapOnNull(expr->type)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool trapOnNull(Expression* expr) { return trapOnNull({expr}); } | |
| void visitBlock(Block* curr) { | |
| if (curr->name.is()) { | |
| parent.breakTargets.erase(curr->name); // these were internal breaks | |
| } | |
| } | |
| void visitIf(If* curr) {} | |
| void visitLoop(Loop* curr) { | |
| if (curr->name.is() && parent.breakTargets.erase(curr->name) > 0) { | |
| parent.mayNotReturn = true; | |
| } | |
| } | |
| void visitBreak(Break* curr) { parent.breakTargets.insert(curr->name); } | |
| void visitSwitch(Switch* curr) { | |
| for (auto name : curr->targets) { | |
| parent.breakTargets.insert(name); | |
| } | |
| parent.breakTargets.insert(curr->default_); | |
| } | |
| void visitCall(Call* curr) { | |
| if (Intrinsics(parent.module).isCallWithoutEffects(curr)) { | |
| // The only effect this can have is to branch (which is an effect of the | |
| // return, not the call). | |
| if (curr->isReturn) { | |
| parent.branchesOut = true; | |
| } | |
| return; | |
| } | |
| const EffectAnalyzer* callTargetEffects = nullptr; | |
| if (auto* target = parent.module.getFunctionOrNull(curr->target); | |
| target && target->effects) { | |
| callTargetEffects = target->effects.get(); | |
| } | |
| addCallEffects(curr, callTargetEffects); | |
| } | |
| void visitCallIndirect(CallIndirect* curr) { | |
| auto* table = parent.module.getTable(curr->table); | |
| if (trapOnNull(table->type)) { | |
| return; | |
| } | |
| if (!Type::isSubType(Type(curr->heapType, Nullability::NonNullable), | |
| table->type)) { | |
| parent.trap = true; | |
| return; | |
| } | |
| // Due to index out of bounds. Type-related traps are handled above and | |
| // may set either implicitTrap or trap (or neither). | |
| parent.implicitTrap = true; | |
| const EffectAnalyzer* callTargetEffects = nullptr; | |
| if (auto it = parent.module.indirectCallEffects.find(curr->heapType); | |
| it != parent.module.indirectCallEffects.end()) { | |
| callTargetEffects = it->second.get(); | |
| } | |
| addCallEffects(curr, callTargetEffects); | |
| } | |
| void visitCallRef(CallRef* curr) { | |
| if (trapOnNull(curr->target)) { | |
| return; | |
| } | |
| const EffectAnalyzer* callTargetEffects = nullptr; | |
| if (auto it = parent.module.indirectCallEffects.find( | |
| curr->target->type.getHeapType()); | |
| it != parent.module.indirectCallEffects.end()) { | |
| callTargetEffects = it->second.get(); | |
| } | |
| addCallEffects(curr, callTargetEffects); | |
| } | |
| void visitLocalGet(LocalGet* curr) { | |
| parent.localsRead.insert(curr->index); | |
| } | |
| void visitLocalSet(LocalSet* curr) { | |
| parent.localsWritten.insert(curr->index); | |
| } | |
| void visitGlobalGet(GlobalGet* curr) { | |
| if (parent.module.getGlobal(curr->name)->mutable_ == Mutable) { | |
| parent.mutableGlobalsRead.insert(curr->name); | |
| } | |
| } | |
| void visitGlobalSet(GlobalSet* curr) { | |
| parent.globalsWritten.insert(curr->name); | |
| } | |
| void visitLoad(Load* curr) { | |
| readsMemory(curr->memory, curr->order); | |
| parent.implicitTrap = true; | |
| } | |
| void visitStore(Store* curr) { | |
| writesMemory(curr->memory, curr->order); | |
| parent.implicitTrap = true; | |
| } | |
| void visitAtomicRMW(AtomicRMW* curr) { | |
| readsMemory(curr->memory, curr->order); | |
| writesMemory(curr->memory, curr->order); | |
| parent.implicitTrap = true; | |
| } | |
| void visitAtomicCmpxchg(AtomicCmpxchg* curr) { | |
| readsMemory(curr->memory, curr->order); | |
| writesMemory(curr->memory, curr->order); | |
| parent.implicitTrap = true; | |
| } | |
| void visitAtomicWait(AtomicWait* curr) { | |
| // Waits on unshared memories trap. | |
| if (!parent.module.getMemory(curr->memory)->shared) { | |
| parent.trap = true; | |
| return; | |
| } | |
| // AtomicWait doesn't strictly write memory, but it does modify the | |
| // waiters list associated with the specified address, which we can think | |
| // of as a write. | |
| parent.readsSharedMemory = true; | |
| parent.writesSharedMemory = true; | |
| parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; | |
| // Traps on unaligned accesses. | |
| parent.implicitTrap = true; | |
| } | |
| void visitAtomicNotify(AtomicNotify* curr) { | |
| // Notifies on unshared memories just return 0 or trap on unaligned | |
| // accesses. | |
| parent.implicitTrap = true; | |
| if (!parent.module.getMemory(curr->memory)->shared) { | |
| return; | |
| } | |
| // AtomicNotify doesn't strictly write memory, but it does | |
| // modify the waiters list associated with the specified address, which we | |
| // can think of as a write. | |
| parent.readsSharedMemory = true; | |
| parent.writesSharedMemory = true; | |
| parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; | |
| } | |
| void visitAtomicFence(AtomicFence* curr) { | |
| // AtomicFence should not be reordered with any shared location accesses, | |
| // so we set these to true. | |
| parent.readsSharedMemory = true; | |
| parent.writesSharedMemory = true; | |
| parent.readsSharedMutableStruct = true; | |
| parent.writesSharedStruct = true; | |
| parent.readsSharedMutableArray = true; | |
| parent.writesSharedArray = true; | |
| parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; | |
| } | |
| void visitPause(Pause* curr) { | |
| // We don't want this to be moved out of loops, but it doesn't otherwises | |
| // matter much how it gets reordered. Say we transfer control as a coarse | |
| // approximation of this. | |
| parent.branchesOut = true; | |
| } | |
| void visitSIMDExtract(SIMDExtract* curr) {} | |
| void visitSIMDReplace(SIMDReplace* curr) {} | |
| void visitSIMDShuffle(SIMDShuffle* curr) {} | |
| void visitSIMDTernary(SIMDTernary* curr) {} | |
| void visitSIMDShift(SIMDShift* curr) {} | |
| void visitSIMDLoad(SIMDLoad* curr) { | |
| readsMemory(curr->memory, MemoryOrder::Unordered); | |
| parent.implicitTrap = true; | |
| } | |
| void visitSIMDLoadStoreLane(SIMDLoadStoreLane* curr) { | |
| if (curr->isLoad()) { | |
| readsMemory(curr->memory, MemoryOrder::Unordered); | |
| } else { | |
| writesMemory(curr->memory, MemoryOrder::Unordered); | |
| } | |
| parent.implicitTrap = true; | |
| } | |
| void visitMemoryInit(MemoryInit* curr) { | |
| writesMemory(curr->memory, MemoryOrder::Unordered); | |
| parent.implicitTrap = true; | |
| } | |
| void visitDataDrop(DataDrop* curr) { | |
| // data.drop does not actually write memory, but it does alter the size of | |
| // a segment, which can be noticeable later by memory.init, so we need to | |
| // mark it as having a global side effect of some kind. | |
| parent.writesMemory = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitMemoryCopy(MemoryCopy* curr) { | |
| readsMemory(curr->sourceMemory, MemoryOrder::Unordered); | |
| writesMemory(curr->destMemory, MemoryOrder::Unordered); | |
| parent.implicitTrap = true; | |
| } | |
| void visitMemoryFill(MemoryFill* curr) { | |
| writesMemory(curr->memory, MemoryOrder::Unordered); | |
| parent.implicitTrap = true; | |
| } | |
| void visitConst(Const* curr) {} | |
| void visitUnary(Unary* curr) { | |
| switch (curr->op) { | |
| case TruncSFloat32ToInt32: | |
| case TruncSFloat32ToInt64: | |
| case TruncUFloat32ToInt32: | |
| case TruncUFloat32ToInt64: | |
| case TruncSFloat64ToInt32: | |
| case TruncSFloat64ToInt64: | |
| case TruncUFloat64ToInt32: | |
| case TruncUFloat64ToInt64: { | |
| parent.implicitTrap = true; | |
| break; | |
| } | |
| default: { | |
| } | |
| } | |
| } | |
| void visitBinary(Binary* curr) { | |
| switch (curr->op) { | |
| case DivSInt32: | |
| case DivUInt32: | |
| case RemSInt32: | |
| case RemUInt32: | |
| case DivSInt64: | |
| case DivUInt64: | |
| case RemSInt64: | |
| case RemUInt64: { | |
| // div and rem may contain implicit trap only if RHS is | |
| // non-constant or constant which equal zero or -1 for | |
| // signed divisions. Reminder traps only with zero | |
| // divider. | |
| if (auto* c = curr->right->dynCast<Const>()) { | |
| if (c->value.isZero()) { | |
| parent.implicitTrap = true; | |
| } else if ((curr->op == DivSInt32 || curr->op == DivSInt64) && | |
| c->value.getInteger() == -1LL) { | |
| parent.implicitTrap = true; | |
| } | |
| } else { | |
| parent.implicitTrap = true; | |
| } | |
| break; | |
| } | |
| default: { | |
| } | |
| } | |
| } | |
| void visitWideIntAddSub(WideIntAddSub* curr) {} | |
| void visitWideIntMul(WideIntMul* curr) {} | |
| void visitSelect(Select* curr) {} | |
| void visitDrop(Drop* curr) {} | |
| void visitReturn(Return* curr) { parent.branchesOut = true; } | |
| void visitMemorySize(MemorySize* curr) { | |
| // memory.size accesses the size of the memory, and thus can be modeled as | |
| // reading memory. When the memory is shared, this synchronizes with | |
| // memory.grow on other threads. | |
| readsMemory(curr->memory, MemoryOrder::SeqCst); | |
| } | |
| void visitMemoryGrow(MemoryGrow* curr) { | |
| // memory.grow technically does a read-modify-write operation on the | |
| // memory size in the successful case, modifying the set of valid | |
| // addresses, and just a read operation in the failure case. It | |
| // synchronizes with memory.size on other threads, but only when operating | |
| // on shared memories. | |
| readsMemory(curr->memory, MemoryOrder::SeqCst); | |
| writesMemory(curr->memory, MemoryOrder::SeqCst); | |
| } | |
| void visitRefNull(RefNull* curr) {} | |
| void visitRefIsNull(RefIsNull* curr) {} | |
| void visitRefFunc(RefFunc* curr) {} | |
| void visitRefEq(RefEq* curr) {} | |
| void visitTableGet(TableGet* curr) { | |
| parent.readsTable = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitTableSet(TableSet* curr) { | |
| parent.writesTable = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitTableSize(TableSize* curr) { parent.readsTable = true; } | |
| void visitTableGrow(TableGrow* curr) { | |
| // table.grow technically does a read-modify-write operation on the | |
| // table size in the successful case, modifying the set of valid | |
| // indices, and just a read operation in the failure case | |
| parent.readsTable = true; | |
| parent.writesTable = true; | |
| } | |
| void visitTableFill(TableFill* curr) { | |
| parent.writesTable = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitTableCopy(TableCopy* curr) { | |
| parent.readsTable = true; | |
| parent.writesTable = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitTableInit(TableInit* curr) { | |
| parent.writesTable = true; | |
| parent.implicitTrap = true; | |
| } | |
| void visitElemDrop(ElemDrop* curr) { parent.writesTable = true; } | |
| void visitTry(Try* curr) { | |
| if (curr->delegateTarget.is()) { | |
| parent.delegateTargets.insert(curr->delegateTarget); | |
| } | |
| } | |
| void visitTryTable(TryTable* curr) { | |
| for (auto name : curr->catchDests) { | |
| parent.breakTargets.insert(name); | |
| } | |
| } | |
| void visitThrow(Throw* curr) { | |
| if (parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| void visitRethrow(Rethrow* curr) { | |
| if (parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| void visitThrowRef(ThrowRef* curr) { | |
| if (trapOnNull(curr->exnref)) { | |
| return; | |
| } | |
| if (parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| void visitNop(Nop* curr) {} | |
| void visitUnreachable(Unreachable* curr) { parent.trap = true; } | |
| void visitPop(Pop* curr) { | |
| if (parent.catchDepth == 0) { | |
| parent.danglingPop = true; | |
| } | |
| } | |
| void visitTupleMake(TupleMake* curr) {} | |
| void visitTupleExtract(TupleExtract* curr) {} | |
| void visitRefI31(RefI31* curr) {} | |
| void visitI31Get(I31Get* curr) { trapOnNull(curr->i31); } | |
| void visitRefTest(RefTest* curr) {} | |
| void visitRefCast(RefCast* curr) { | |
| if (trapOnNull(curr->desc)) { | |
| return; | |
| } | |
| // Traps if the cast fails. | |
| parent.implicitTrap = true; | |
| } | |
| void visitRefGetDesc(RefGetDesc* curr) { trapOnNull(curr->ref); } | |
| void visitBrOn(BrOn* curr) { | |
| if (trapOnNull(curr->desc)) { | |
| return; | |
| } | |
| parent.breakTargets.insert(curr->name); | |
| } | |
| void visitStructNew(StructNew* curr) { trapOnNull(curr->desc); } | |
| void visitStructGet(StructGet* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| readsStruct(curr->ref->type.getHeapType(), curr->index, curr->order); | |
| } | |
| void visitStructSet(StructSet* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| writesStruct(curr->ref->type.getHeapType(), curr->index, curr->order); | |
| } | |
| template<typename StructRMWExpr> | |
| void visitStructRMWExpr(StructRMWExpr* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| auto heapType = curr->ref->type.getHeapType(); | |
| readsStruct(heapType, curr->index, curr->order); | |
| writesStruct(heapType, curr->index, curr->order); | |
| } | |
| void visitStructRMW(StructRMW* curr) { visitStructRMWExpr(curr); } | |
| void visitStructCmpxchg(StructCmpxchg* curr) { visitStructRMWExpr(curr); } | |
| void visitStructWait(StructWait* curr) { | |
| if (trapOnNull({curr->ref, curr->waitqueue})) { | |
| return; | |
| } | |
| // StructWait doesn't strictly write a struct, but it does modify the | |
| // waiters list associated with the waitqueue, which we can think | |
| // of as a write. | |
| parent.readsSharedMutableStruct = true; | |
| parent.writesSharedStruct = true; | |
| parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; | |
| // If the timeout is negative and no-one wakes us. | |
| parent.mayNotReturn = true; | |
| } | |
| void visitWaitqueueNew(WaitqueueNew* curr) {} | |
| void visitWaitqueueNotify(WaitqueueNotify* curr) { | |
| if (trapOnNull(curr->waitqueue)) { | |
| return; | |
| } | |
| // WaitqueueNotify doesn't strictly write anything, but it does | |
| // modify the waiters list associated with the waitqueue, which we | |
| // can think of as a write. | |
| parent.readsSharedMutableStruct = true; | |
| parent.writesSharedStruct = true; | |
| parent.readOrder = parent.writeOrder = MemoryOrder::SeqCst; | |
| } | |
| void visitArrayNew(ArrayNew* curr) {} | |
| void visitArrayNewData(ArrayNewData* curr) { | |
| // Traps on out of bounds access to segments or access to dropped | |
| // segments. | |
| parent.implicitTrap = true; | |
| } | |
| void visitArrayNewElem(ArrayNewElem* curr) { | |
| // Traps on out of bounds access to segments or access to dropped | |
| // segments. | |
| parent.implicitTrap = true; | |
| } | |
| void visitArrayNewFixed(ArrayNewFixed* curr) {} | |
| void visitArrayGet(ArrayGet* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| readsArray(curr->ref->type.getHeapType(), curr->order); | |
| } | |
| void visitArraySet(ArraySet* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| writesArray(curr->ref->type.getHeapType(), curr->order); | |
| } | |
| void visitArrayLoad(ArrayLoad* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| readsArray(curr->ref->type.getHeapType(), MemoryOrder::Unordered); | |
| } | |
| void visitArrayStore(ArrayStore* curr) { | |
| if (curr->ref->type.isNull()) { | |
| parent.trap = true; | |
| return; | |
| } | |
| parent.writesArray = true; | |
| // traps when the arg is null or the index out of bounds | |
| parent.implicitTrap = true; | |
| } | |
| void visitArrayLen(ArrayLen* curr) { | |
| trapOnNull(curr->ref); | |
| // No need to model this as reading the array since the length cannot be | |
| // written, so there can be no conflicts. | |
| } | |
| void visitArrayCopy(ArrayCopy* curr) { | |
| if (trapOnNull({curr->srcRef, curr->destRef})) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| readsArray(curr->srcRef->type.getHeapType(), MemoryOrder::Unordered); | |
| writesArray(curr->destRef->type.getHeapType(), MemoryOrder::Unordered); | |
| } | |
| void visitArrayFill(ArrayFill* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| writesArray(curr->ref->type.getHeapType(), MemoryOrder::Unordered); | |
| } | |
| template<typename ArrayInit> void visitArrayInit(ArrayInit* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| parent.writesArray = true; | |
| // OOB access to array or element segment. | |
| parent.implicitTrap = true; | |
| writesArray(curr->ref->type.getHeapType(), MemoryOrder::Unordered); | |
| } | |
| void visitArrayInitData(ArrayInitData* curr) { visitArrayInit(curr); } | |
| void visitArrayInitElem(ArrayInitElem* curr) { visitArrayInit(curr); } | |
| template<typename ArrayRMWExpr> void visitArrayRMWExpr(ArrayRMWExpr* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // Null refs and OOB access. | |
| parent.implicitTrap = true; | |
| auto heapType = curr->ref->type.getHeapType(); | |
| readsArray(heapType, curr->order); | |
| writesArray(heapType, curr->order); | |
| } | |
| void visitArrayRMW(ArrayRMW* curr) { visitArrayRMWExpr(curr); } | |
| void visitArrayCmpxchg(ArrayCmpxchg* curr) { visitArrayRMWExpr(curr); } | |
| void visitRefAs(RefAs* curr) { | |
| if (curr->op == AnyConvertExtern || curr->op == ExternConvertAny) { | |
| // These conversions are infallible. | |
| return; | |
| } | |
| assert(curr->op == RefAsNonNull); | |
| trapOnNull(curr->value); | |
| } | |
| void visitStringNew(StringNew* curr) { | |
| switch (curr->op) { | |
| case StringNewLossyUTF8Array: | |
| case StringNewWTF16Array: | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // OOB access to array. | |
| parent.implicitTrap = true; | |
| parent.readsMutableArray = true; | |
| return; | |
| case StringNewFromCodePoint: | |
| // Invalid code points. | |
| parent.implicitTrap = true; | |
| return; | |
| } | |
| } | |
| void visitStringConst(StringConst* curr) {} | |
| void visitStringMeasure(StringMeasure* curr) { trapOnNull(curr->ref); } | |
| void visitStringEncode(StringEncode* curr) { | |
| if (trapOnNull({curr->str, curr->array})) { | |
| return; | |
| } | |
| // OOB array access. | |
| parent.implicitTrap = true; | |
| parent.writesArray = true; | |
| } | |
| void visitStringConcat(StringConcat* curr) { | |
| trapOnNull({curr->left, curr->right}); | |
| } | |
| void visitStringEq(StringEq* curr) { | |
| switch (curr->op) { | |
| case StringEqEqual: | |
| // Nulls are ok. | |
| return; | |
| case StringEqCompare: | |
| trapOnNull({curr->left, curr->right}); | |
| return; | |
| } | |
| } | |
| void visitStringTest(StringTest* curr) {} | |
| void visitStringWTF16Get(StringWTF16Get* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // OOB string access. | |
| parent.implicitTrap = true; | |
| } | |
| void visitStringSliceWTF(StringSliceWTF* curr) { | |
| if (trapOnNull(curr->ref)) { | |
| return; | |
| } | |
| // OOB string access. | |
| parent.implicitTrap = true; | |
| } | |
| void visitContNew(ContNew* curr) { trapOnNull(curr->func); } | |
| void visitContBind(ContBind* curr) { | |
| if (trapOnNull(curr->cont)) { | |
| return; | |
| } | |
| // The input continuation is modified, as it will trap if resumed. This is | |
| // a globally-noticeable effect, which we model as a call for now, but we | |
| // could in theory use something more refined here (|modifiesContinuation| | |
| // perhaps, to parallel |writesMemory| etc.). | |
| parent.calls = true; | |
| } | |
| void visitSuspend(Suspend* curr) { | |
| // Similar to resume/call: Suspending means that we execute arbitrary | |
| // other code before we may resume here. | |
| parent.calls = true; | |
| if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| // A suspend may go unhandled and therefore trap. | |
| parent.implicitTrap = true; | |
| } | |
| void visitResume(Resume* curr) { | |
| if (trapOnNull(curr->cont)) { | |
| return; | |
| } | |
| // This acts as a kitchen sink effect. | |
| parent.calls = true; | |
| if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| void visitResumeThrow(ResumeThrow* curr) { | |
| if (trapOnNull(curr->cont)) { | |
| return; | |
| } | |
| // This acts as a kitchen sink effect. | |
| parent.calls = true; | |
| if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| void visitStackSwitch(StackSwitch* curr) { | |
| if (trapOnNull(curr->cont)) { | |
| return; | |
| } | |
| // This acts as a kitchen sink effect. | |
| parent.calls = true; | |
| if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| private: | |
| // Populate a call's effects using effects computed from GlobalEffects. Note | |
| // that calls may have other effects that aren't captured by the function | |
| // body of the target (e.g. a call_ref may trap on null refs). | |
| template<typename CallType> | |
| void addCallEffectsFromGlobalEffects(const CallType* curr, | |
| const EffectAnalyzer& funcEffects) { | |
| if (curr->isReturn) { | |
| if (funcEffects.throws()) { | |
| parent.hasReturnCallThrow = true; | |
| } | |
| } | |
| if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) { | |
| // We can ignore a throw here, as the parent catches it. | |
| // | |
| // Also, we can filter out throws for return calls because they are | |
| // already more precisely captured by `branchesOut`, which models the | |
| // return, and `hasReturnCallThrow`, which models the throw that will | |
| // happen after the return. | |
| auto filteredEffects = funcEffects; | |
| filteredEffects.throws_ = false; | |
| parent.mergeIn(filteredEffects); | |
| } else { | |
| parent.mergeIn(funcEffects); | |
| } | |
| } | |
| // Common effects logic for the 3 types of call: `call`, `call_indirect`, | |
| // and `call_ref`. | |
| template<typename CallType> | |
| void addCallEffects(const CallType* curr, | |
| const EffectAnalyzer* callTargetEffects) { | |
| if (curr->isReturn) { | |
| parent.branchesOut = true; | |
| } | |
| if (callTargetEffects) { | |
| addCallEffectsFromGlobalEffects(curr, *callTargetEffects); | |
| return; | |
| } | |
| parent.calls = true; | |
| // If EH is enabled and we don't have global effects information, | |
| // assume that the call target may throw. | |
| if (parent.features.hasExceptionHandling()) { | |
| if (curr->isReturn) { | |
| parent.hasReturnCallThrow = true; | |
| } | |
| if (parent.tryDepth == 0 && !curr->isReturn) { | |
| parent.throws_ = true; | |
| } | |
| } | |
| } | |
| }; | |
| public: | |
| // Helpers | |
| // See comment on orderedBefore() for the assumptions on the inputs here. | |
| // TODO: Update users so we can check just one direction here. | |
| static bool canReorder(const PassOptions& passOptions, | |
| Module& module, | |
| Expression* a, | |
| Expression* b) { | |
| EffectAnalyzer aEffects(passOptions, module, a); | |
| EffectAnalyzer bEffects(passOptions, module, b); | |
| return !aEffects.orderedBefore(bEffects) && | |
| !bEffects.orderedBefore(aEffects); | |
| } | |
| // C-API | |
| enum SideEffects : uint32_t { | |
| None = 0, | |
| Branches = 1 << 0, | |
| Calls = 1 << 1, | |
| ReadsLocal = 1 << 2, | |
| WritesLocal = 1 << 3, | |
| ReadsGlobal = 1 << 4, | |
| WritesGlobal = 1 << 5, | |
| ReadsMemory = 1 << 6, | |
| WritesMemory = 1 << 7, | |
| ReadsTable = 1 << 8, | |
| WritesTable = 1 << 9, | |
| ImplicitTrap = 1 << 10, | |
| IsAtomic = 1 << 11, | |
| Throws = 1 << 12, | |
| DanglingPop = 1 << 13, | |
| TrapsNeverHappen = 1 << 14, | |
| Any = (1 << 15) - 1 | |
| }; | |
| uint32_t getSideEffects() const { | |
| uint32_t effects = 0; | |
| if (branchesOut || hasExternalBreakTargets()) { | |
| effects |= SideEffects::Branches; | |
| } | |
| if (calls) { | |
| effects |= SideEffects::Calls; | |
| } | |
| if (localsRead.size() > 0) { | |
| effects |= SideEffects::ReadsLocal; | |
| } | |
| if (localsWritten.size() > 0) { | |
| effects |= SideEffects::WritesLocal; | |
| } | |
| if (mutableGlobalsRead.size()) { | |
| effects |= SideEffects::ReadsGlobal; | |
| } | |
| if (globalsWritten.size() > 0) { | |
| effects |= SideEffects::WritesGlobal; | |
| } | |
| // TODO: Expand the C API to handle shared locations, structs, and arrays. | |
| if (readsMemory || readsSharedMemory) { | |
| effects |= SideEffects::ReadsMemory; | |
| } | |
| if (writesMemory || writesSharedMemory) { | |
| effects |= SideEffects::WritesMemory; | |
| } | |
| if (readsTable) { | |
| effects |= SideEffects::ReadsTable; | |
| } | |
| if (writesTable) { | |
| effects |= SideEffects::WritesTable; | |
| } | |
| if (implicitTrap) { | |
| effects |= SideEffects::ImplicitTrap; | |
| } | |
| if (trapsNeverHappen) { | |
| effects |= SideEffects::TrapsNeverHappen; | |
| } | |
| // TODO: Expand the C API to handle memory orders. | |
| if (readOrder != MemoryOrder::Unordered || | |
| writeOrder != MemoryOrder::Unordered) { | |
| effects |= SideEffects::IsAtomic; | |
| } | |
| if (throws_) { | |
| effects |= SideEffects::Throws; | |
| } | |
| if (danglingPop) { | |
| effects |= SideEffects::DanglingPop; | |
| } | |
| return effects; | |
| } | |
| // Ignores all forms of control flow transfers: breaks, returns, and | |
| // exceptions. (Note that traps are not considered relevant here - a trap does | |
| // not just transfer control flow, but can be seen as halting the entire | |
| // program.) | |
| // | |
| // This function matches transfersControlFlow(), that is, after calling this | |
| // method transfersControlFlow() will always return false. | |
| void ignoreControlFlowTransfers() { | |
| branchesOut = false; | |
| breakTargets.clear(); | |
| throws_ = false; | |
| delegateTargets.clear(); | |
| assert(!transfersControlFlow()); | |
| } | |
| private: | |
| void post() { | |
| assert(tryDepth == 0); | |
| if (ignoreImplicitTraps) { | |
| implicitTrap = false; | |
| } else if (implicitTrap) { | |
| trap = true; | |
| } | |
| } | |
| }; | |
| // Calculate effects only on the node itself (shallowly), and not on | |
| // children. | |
| class ShallowEffectAnalyzer : public EffectAnalyzer { | |
| public: | |
| ShallowEffectAnalyzer(const PassOptions& passOptions, | |
| Module& module, | |
| Expression* ast = nullptr) | |
| : EffectAnalyzer(passOptions, module) { | |
| if (ast) { | |
| visit(ast); | |
| } | |
| } | |
| }; | |
| std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects); | |
| } // namespace wasm | |
| #endif // wasm_ir_effects_h |