/*
 * 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 "ir/intrinsics.h"
#include "pass.h"
#include "support/name.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, 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,
                 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;

  Module& module;
  FeatureSet features;

  std::set<Index> localsRead;
  std::set<Index> localsWritten;
  std::set<Name> mutableGlobalsRead;
  std::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. That is, we assume
  // that there is no transfer of control flow *between* them: we are comparing
  // things appear after each other, perhaps with some other code in the middle,
  // but that code does not transfer control flow. 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
  //
  // That the things being compared 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. In the second
  // example we can't reorder A and B if B traps, but in the first example 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.count(local) || other.localsWritten.count(local)) {
        return true;
      }
    }
    for (auto local : localsRead) {
      if (other.localsWritten.count(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.count(global) ||
          other.globalsWritten.count(global)) {
        return true;
      }
    }
    for (auto global : mutableGlobalsRead) {
      if (other.globalsWritten.count(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::set<Name> breakTargets;
  std::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.count(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 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 && expr->type == Type::unreachable) {
          return true;
        }
      }
      for (auto* expr : exprs) {
        assert(!expr || expr->type.isRef());
        if (expr && expr->type.isNull()) {
          parent.trap = true;
          return true;
        }
      }
      for (auto* expr : exprs) {
        if (expr && expr->type.isNullable()) {
          parent.implicitTrap = true;
          break;
        }
      }
      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) {
      // call.without.effects has no effects.
      if (Intrinsics(parent.module).isCallWithoutEffects(curr)) {
        return;
      }

      // Get the target's effects, if they exist. Note that we must handle the
      // case of the function not yet existing (we may be executed in the middle
      // of a pass, which may have built up calls but not the targets of those
      // calls; in such a case, we do not find the targets and therefore assume
      // we know nothing about the effects, which is safe).
      const EffectAnalyzer* targetEffects = nullptr;
      if (auto* target = parent.module.getFunctionOrNull(curr->target)) {
        targetEffects = target->effects.get();
      }

      if (curr->isReturn) {
        parent.branchesOut = true;
        // When EH is enabled, any call can throw.
        if (parent.features.hasExceptionHandling() &&
            (!targetEffects || targetEffects->throws())) {
          parent.hasReturnCallThrow = true;
        }
      }

      if (targetEffects) {
        // We have effect information for this call target, and can just use
        // that. The one change we may want to make is to remove throws_, if the
        // target function throws and we know that will be caught anyhow, the
        // same as the code below for the general path. We can always 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.
        if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
          auto filteredEffects = *targetEffects;
          filteredEffects.throws_ = false;
          parent.mergeIn(filteredEffects);
        } else {
          // Just merge in all the effects.
          parent.mergeIn(*targetEffects);
        }
        return;
      }

      parent.calls = true;
      // When EH is enabled, any call can throw. Skip this for return calls
      // because the throw is already more precisely captured by the combination
      // of `hasReturnCallThrow` and `branchesOut`.
      if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 &&
          !curr->isReturn) {
        parent.throws_ = true;
      }
    }
    void visitCallIndirect(CallIndirect* curr) {
      parent.calls = true;
      if (curr->isReturn) {
        parent.branchesOut = true;
        if (parent.features.hasExceptionHandling()) {
          parent.hasReturnCallThrow = true;
        }
      }
      if (parent.features.hasExceptionHandling() &&
          (parent.tryDepth == 0 && !curr->isReturn)) {
        parent.throws_ = true;
      }
    }
    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.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 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 visitCallRef(CallRef* curr) {
      if (trapOnNull(curr->target)) {
        return;
      }
      if (curr->isReturn) {
        parent.branchesOut = true;
        if (parent.features.hasExceptionHandling()) {
          parent.hasReturnCallThrow = true;
        }
      }
      parent.calls = true;
      if (parent.features.hasExceptionHandling() &&
          (parent.tryDepth == 0 && !curr->isReturn)) {
        parent.throws_ = true;
      }
    }
    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)) {
        return;
      }
      // StructWait doesn't strictly write a struct, but it does modify the
      // waiters list associated with the waitqueue field, 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 visitStructNotify(StructNotify* curr) {
      if (trapOnNull(curr->ref)) {
        return;
      }
      // Non-shared notifies just return 0.
      if (curr->ref->type.getHeapType().isShared()) {
        return;
      }
      // AtomicNotify doesn't strictly write the struct, but it does
      // modify the waiters list associated with the waitqueue field, 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 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;
      }
    }
  };

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);
    }
  }
};

} // namespace wasm

namespace std {
std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects);
} // namespace std

#endif // wasm_ir_effects_h
