/*
 * 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.
 */

//
// Lowers i64s to i32s by splitting variables and arguments
// into pairs of i32s. i64 return values are lowered by
// returning the low half and storing the high half into a
// global.
//

#include <algorithm>
#include "wasm.h"
#include "pass.h"
#include "emscripten-optimizer/istring.h"
#include "support/name.h"
#include "wasm-builder.h"
#include "ir/names.h"


namespace wasm {

static Name makeHighName(Name n) {
  return Name(
    cashew::IString((std::string(n.c_str()) + "$hi").c_str(), false)
  );
}

struct I64ToI32Lowering : public WalkerPass<PostWalker<I64ToI32Lowering>> {
  struct TempVar {
    TempVar(Index idx, I64ToI32Lowering& pass) :
      idx(idx), pass(pass), moved(false) {}

    TempVar(TempVar&& other) : idx(other), pass(other.pass), moved(false) {
      assert(!other.moved);
      other.moved = true;
    }

    TempVar& operator=(TempVar&& rhs) {
      assert(!rhs.moved);
      // free overwritten idx
      if (!moved) freeIdx();
      idx = rhs.idx;
      rhs.moved = true;
      moved = false;
      return *this;
    }

    ~TempVar() {
      if (!moved) freeIdx();
    }

    bool operator==(const TempVar& rhs) {
      assert(!moved && !rhs.moved);
      return idx == rhs.idx;
    }

    operator Index() {
      assert(!moved);
      return idx;
    }

    // disallow copying
    TempVar(const TempVar&) = delete;
    TempVar& operator=(const TempVar&) = delete;

  private:
    void freeIdx() {
      assert(std::find(pass.freeTemps.begin(), pass.freeTemps.end(), idx) ==
             pass.freeTemps.end());
      pass.freeTemps.push_back(idx);
    }

    Index idx;
    I64ToI32Lowering& pass;
    bool moved; // since C++ will still destruct moved-from values
  };

  static Name highBitsGlobal;

  // false since function types need to be lowered
  // TODO: allow module-level transformations in parallel passes
  bool isFunctionParallel() override { return false; }

  Pass* create() override {
    return new I64ToI32Lowering;
  }

  void doWalkModule(Module* module) {
    if (!builder) builder = make_unique<Builder>(*module);
    // add new globals for high bits
    for (size_t i = 0, globals = module->globals.size(); i < globals; ++i) {
      auto& curr = module->globals[i];
      if (curr->type != i64) continue;
      curr->type = i32;
      auto* high = new Global(*curr);
      high->name = makeHighName(curr->name);
      module->addGlobal(high);
    }
    PostWalker<I64ToI32Lowering>::doWalkModule(module);
  }

  void visitFunctionType(FunctionType* curr) {
    std::vector<WasmType> params;
    for (auto t : curr->params) {
      if (t == i64) {
        params.push_back(i32);
        params.push_back(i32);
      } else {
        params.push_back(t);
      }
    }
    std::swap(params, curr->params);
    if (curr->result == i64) {
      curr->result = i32;
    }
  }

  void doWalkFunction(Function* func) {
    // create builder here if this is first entry to module for this object
    if (!builder) builder = make_unique<Builder>(*getModule());
    indexMap.clear();
    highBitVars.clear();
    labelHighBitVars.clear();
    freeTemps.clear();
    Function oldFunc(*func);
    func->params.clear();
    func->vars.clear();
    func->localNames.clear();
    func->localIndices.clear();
    Index newIdx = 0;
    Names::ensureNames(&oldFunc);
    for (Index i = 0; i < oldFunc.getNumLocals(); ++i) {
      assert(oldFunc.hasLocalName(i));
      Name lowName = oldFunc.getLocalName(i);
      Name highName = makeHighName(lowName);
      WasmType paramType = oldFunc.getLocalType(i);
      auto builderFunc = (i < oldFunc.getVarIndexBase()) ?
          Builder::addParam :
          static_cast<Index (*)(Function*, Name, WasmType)>(Builder::addVar);
      if (paramType == i64) {
        builderFunc(func, lowName, i32);
        builderFunc(func, highName, i32);
        indexMap[i] = newIdx;
        newIdx += 2;
      } else {
        builderFunc(func, lowName, paramType);
        indexMap[i] = newIdx++;
      }
    }
    nextTemp = func->getNumLocals();
    PostWalker<I64ToI32Lowering>::doWalkFunction(func);
  }

  void visitFunction(Function* func) {
    if (func->result == i64) {
      func->result = i32;
      // body may not have out param if it ends with control flow
      if (hasOutParam(func->body)) {
        TempVar highBits = fetchOutParam(func->body);
        TempVar lowBits = getTemp();
        SetLocal* setLow = builder->makeSetLocal(
          lowBits,
          func->body
        );
        SetGlobal* setHigh = builder->makeSetGlobal(
          highBitsGlobal,
          builder->makeGetLocal(highBits, i32)
        );
        GetLocal* getLow = builder->makeGetLocal(lowBits, i32);
        func->body = builder->blockify(setLow, setHigh, getLow);
      }
    }
    assert(freeTemps.size() == nextTemp - func->getNumLocals());
    int idx = 0;
    for (size_t i = func->getNumLocals(); i < nextTemp; i++) {
      Name tmpName("i64toi32_i32$" + std::to_string(idx++));
      builder->addVar(func, tmpName, i32);
    }
  }

  void visitBlock(Block* curr) {
    if (curr->list.size() == 0) return;
    if (curr->type == i64) curr->type = i32;
    auto highBitsIt = labelHighBitVars.find(curr->name);
    if (!hasOutParam(curr->list.back())) {
      if (highBitsIt != labelHighBitVars.end()) {
        setOutParam(curr, std::move(highBitsIt->second));
      }
      return;
    }
    TempVar lastHighBits = fetchOutParam(curr->list.back());
    if (highBitsIt == labelHighBitVars.end() ||
        highBitsIt->second == lastHighBits) {
      setOutParam(curr, std::move(lastHighBits));
      if (highBitsIt != labelHighBitVars.end()) {
        labelHighBitVars.erase(highBitsIt);
      }
      return;
    }
    TempVar highBits = std::move(highBitsIt->second);
    TempVar tmp = getTemp();
    labelHighBitVars.erase(highBitsIt);
    SetLocal* setLow = builder->makeSetLocal(tmp, curr->list.back());
    SetLocal* setHigh = builder->makeSetLocal(
      highBits,
      builder->makeGetLocal(lastHighBits, i32)
    );
    GetLocal* getLow = builder->makeGetLocal(tmp, i32);
    curr->list.back() = builder->blockify(setLow, setHigh, getLow);
    setOutParam(curr, std::move(highBits));
  }

  // If and Select have identical code
  template <typename T>
  void visitBranching(T* curr) {
    if (!hasOutParam(curr->ifTrue)) return;
    assert(curr->ifFalse != nullptr && "Nullable ifFalse found");
    TempVar highBits = fetchOutParam(curr->ifTrue);
    TempVar falseBits = fetchOutParam(curr->ifFalse);
    TempVar tmp = getTemp();
    curr->type = i32;
    curr->ifFalse = builder->blockify(
      builder->makeSetLocal(tmp, curr->ifFalse),
      builder->makeSetLocal(
        highBits,
        builder->makeGetLocal(falseBits, i32)
      ),
      builder->makeGetLocal(tmp, i32)
    );
    setOutParam(curr, std::move(highBits));
  }

  void visitIf(If* curr) {
    visitBranching<If>(curr);
  }

  void visitLoop(Loop* curr) {
    assert(labelHighBitVars.find(curr->name) == labelHighBitVars.end());
    if (curr->type != i64) return;
    curr->type = i32;
    setOutParam(curr, fetchOutParam(curr->body));
  }

  void visitBreak(Break* curr) {
    if (!hasOutParam(curr->value)) return;
    assert(curr->value != nullptr);
    TempVar valHighBits = fetchOutParam(curr->value);
    auto blockHighBitsIt = labelHighBitVars.find(curr->name);
    if (blockHighBitsIt == labelHighBitVars.end()) {
      labelHighBitVars.emplace(curr->name, std::move(valHighBits));
      curr->type = i32;
      return;
    }
    TempVar blockHighBits = std::move(blockHighBitsIt->second);
    TempVar tmp = getTemp();
    SetLocal* setLow = builder->makeSetLocal(tmp, curr->value);
    SetLocal* setHigh = builder->makeSetLocal(
      blockHighBits,
      builder->makeGetLocal(valHighBits, i32)
    );
    curr->value = builder->makeGetLocal(tmp, i32);
    curr->type = i32;
    replaceCurrent(builder->blockify(setLow, setHigh, curr));
  }

  void visitSwitch(Switch* curr) {
    if (!hasOutParam(curr->value)) return;
    TempVar outParam = fetchOutParam(curr->value);
    TempVar tmp = getTemp();
    Expression* result = curr;
    std::vector<Name> targets;
    size_t blockID = 0;
    auto processTarget = [&](Name target) -> Name {
      auto labelIt = labelHighBitVars.find(target);
      if (labelIt == labelHighBitVars.end()) {
        labelHighBitVars.emplace(target, getTemp());
        labelIt = labelHighBitVars.find(target);
      }
      Name newLabel("$i64toi32_" + std::string(target.c_str()) +
                    "_" + std::to_string(blockID++));
      Block* trampoline = builder->makeBlock(newLabel, result);
      trampoline->type = i32;
      result = builder->blockify(
        builder->makeSetLocal(tmp, trampoline),
        builder->makeSetLocal(
          labelIt->second,
          builder->makeGetLocal(outParam, i32)
        ),
        builder->makeBreak(target, builder->makeGetLocal(tmp, i32))
      );
      return newLabel;
    };
    for (auto target : curr->targets) {
      targets.push_back(processTarget(target));
    }
    curr->targets.set(targets);
    curr->default_ = processTarget(curr->default_);
    replaceCurrent(result);
  }

  template <typename T>
  using BuilderFunc = std::function<T*(std::vector<Expression*>&, WasmType)>;

  template <typename T>
  void visitGenericCall(T* curr, BuilderFunc<T> callBuilder) {
    std::vector<Expression*> args;
    for (auto* e : curr->operands) {
      args.push_back(e);
      if (hasOutParam(e)) {
        TempVar argHighBits = fetchOutParam(e);
        args.push_back(builder->makeGetLocal(argHighBits, i32));
      }
    }
    if (curr->type != i64) {
      replaceCurrent(callBuilder(args, curr->type));
      return;
    }
    TempVar lowBits = getTemp();
    TempVar highBits = getTemp();
    SetLocal* doCall = builder->makeSetLocal(
      lowBits,
      callBuilder(args, i32)
    );
    SetLocal* setHigh = builder->makeSetLocal(
      highBits,
      builder->makeGetGlobal(highBitsGlobal, i32)
    );
    GetLocal* getLow = builder->makeGetLocal(lowBits, i32);
    Block* result = builder->blockify(doCall, setHigh, getLow);
    setOutParam(result, std::move(highBits));
    replaceCurrent(result);
  }
  void visitCall(Call* curr) {
    visitGenericCall<Call>(
      curr,
      [&](std::vector<Expression*>& args, WasmType ty) {
        return builder->makeCall(curr->target, args, ty);
      }
    );
  }

  void visitCallImport(CallImport* curr) {
    // imports cannot contain i64s
    return;
  }

  void visitCallIndirect(CallIndirect* curr) {
    visitGenericCall<CallIndirect>(
      curr,
      [&](std::vector<Expression*>& args, WasmType ty) {
        return builder->makeCallIndirect(
          curr->fullType,
          curr->target,
          args,
          ty
        );
      }
    );
  }

  void visitGetLocal(GetLocal* curr) {
    if (curr->type != i64) return;
    curr->index = indexMap[curr->index];
    curr->type = i32;
    TempVar highBits = getTemp();
    SetLocal *setHighBits = builder->makeSetLocal(
      highBits,
      builder->makeGetLocal(
        curr->index + 1,
        i32
      )
    );
    Block* result = builder->blockify(setHighBits, curr);
    replaceCurrent(result);
    setOutParam(result, std::move(highBits));
  }

  void lowerTee(SetLocal* curr) {
    TempVar highBits = fetchOutParam(curr->value);
    TempVar tmp = getTemp();
    curr->index = indexMap[curr->index];
    curr->type = i32;
    SetLocal* setLow = builder->makeSetLocal(tmp, curr);
    SetLocal* setHigh = builder->makeSetLocal(
      curr->index + 1,
      builder->makeGetLocal(highBits, i32)
    );
    GetLocal* getLow = builder->makeGetLocal(tmp, i32);
    Block* result = builder->blockify(setLow, setHigh, getLow);
    replaceCurrent(result);
    setOutParam(result, std::move(highBits));
  }

  void visitSetLocal(SetLocal* curr) {
    if (!hasOutParam(curr->value)) return;
    if (curr->isTee()) {
      lowerTee(curr);
      return;
    }
    TempVar highBits = fetchOutParam(curr->value);
    curr->index = indexMap[curr->index];
    SetLocal* setHigh = builder->makeSetLocal(
      curr->index + 1,
      builder->makeGetLocal(highBits, i32)
    );
    Block* result = builder->blockify(curr, setHigh);
    replaceCurrent(result);
  }

  void visitGetGlobal(GetGlobal* curr) {
    assert(false && "GetGlobal not implemented");
  }

  void visitSetGlobal(SetGlobal* curr) {
    assert(false && "SetGlobal not implemented");
  }

  void visitLoad(Load* curr) {
    if (curr->type != i64) return;
    assert(!curr->isAtomic && "atomic load not implemented");
    TempVar highBits = getTemp();
    TempVar ptrTemp = getTemp();
    SetLocal* setPtr = builder->makeSetLocal(ptrTemp, curr->ptr);
    SetLocal* loadHigh;
    if (curr->bytes == 8) {
      loadHigh = builder->makeSetLocal(
        highBits,
        builder->makeLoad(
          4,
          curr->signed_,
          curr->offset + 4,
          1,
          builder->makeGetLocal(ptrTemp, i32),
          i32
        )
      );
    } else {
      loadHigh = builder->makeSetLocal(
        highBits,
        builder->makeConst(Literal(int32_t(0)))
      );
    }
    curr->type = i32;
    curr->bytes = std::min(curr->bytes, uint8_t(4));
    curr->align = std::min(uint32_t(curr->align), uint32_t(4));
    curr->ptr = builder->makeGetLocal(ptrTemp, i32);
    Block* result = builder->blockify(setPtr, loadHigh, curr);
    replaceCurrent(result);
    setOutParam(result, std::move(highBits));
  }

  void visitStore(Store* curr) {
    if (!hasOutParam(curr->value)) return;
    assert(curr->offset + 4 > curr->offset);
    assert(!curr->isAtomic && "atomic store not implemented");
    TempVar highBits = fetchOutParam(curr->value);
    uint8_t bytes = curr->bytes;
    curr->bytes = std::min(curr->bytes, uint8_t(4));
    curr->align = std::min(uint32_t(curr->align), uint32_t(4));
    curr->valueType = i32;
    if (bytes == 8) {
      TempVar ptrTemp = getTemp();
      SetLocal* setPtr = builder->makeSetLocal(ptrTemp, curr->ptr);
      curr->ptr = builder->makeGetLocal(ptrTemp, i32);
      Store* storeHigh = builder->makeStore(
        4,
        curr->offset + 4,
        1,
        builder->makeGetLocal(ptrTemp, i32),
        builder->makeGetLocal(highBits, i32),
        i32
      );
      replaceCurrent(builder->blockify(setPtr, curr, storeHigh));
    }
  }

  void visitAtomicRMW(AtomicRMW* curr) {
    assert(false && "AtomicRMW not implemented");
  }

  void visitAtomicCmpxchg(AtomicCmpxchg* curr) {
    assert(false && "AtomicCmpxchg not implemented");
  }

  void visitConst(Const* curr) {
    if (curr->type != i64) return;
    TempVar highBits = getTemp();
    Const* lowVal = builder->makeConst(
      Literal(int32_t(curr->value.geti64() & 0xffffffff))
    );
    SetLocal* setHigh = builder->makeSetLocal(
      highBits,
      builder->makeConst(
        Literal(int32_t(uint64_t(curr->value.geti64()) >> 32))
      )
    );
    Block* result = builder->blockify(setHigh, lowVal);
    setOutParam(result, std::move(highBits));
    replaceCurrent(result);
  }

  void lowerEqZInt64(Unary* curr) {
    TempVar highBits = fetchOutParam(curr->value);
    replaceCurrent(
      builder->makeBinary(
        AndInt32,
        builder->makeUnary(EqZInt32, builder->makeGetLocal(highBits, i32)),
        builder->makeUnary(EqZInt32, curr->value)
      )
    );
  }

  void lowerExtendUInt32(Unary* curr) {
    TempVar highBits = getTemp();
    Block* result = builder->blockify(
      builder->makeSetLocal(highBits, builder->makeConst(Literal(int32_t(0)))),
      curr->value
    );
    setOutParam(result, std::move(highBits));
    replaceCurrent(result);
  }

  void lowerWrapInt64(Unary* curr) {
    // free the temp var
    fetchOutParam(curr->value);
    replaceCurrent(curr->value);
  }

  bool unaryNeedsLowering(UnaryOp op) {
    switch (op) {
      case ClzInt64:
      case CtzInt64:
      case PopcntInt64:
      case EqZInt64:
      case ExtendSInt32:
      case ExtendUInt32:
      case WrapInt64:
      case TruncSFloat32ToInt64:
      case TruncUFloat32ToInt64:
      case TruncSFloat64ToInt64:
      case TruncUFloat64ToInt64:
      case ReinterpretFloat64:
      case ConvertSInt64ToFloat32:
      case ConvertSInt64ToFloat64:
      case ConvertUInt64ToFloat32:
      case ConvertUInt64ToFloat64:
      case ReinterpretInt64: return true;
      default: return false;
    }
  }

  void visitUnary(Unary* curr) {
    if (!unaryNeedsLowering(curr->op)) return;
    if (curr->type == unreachable || curr->value->type == unreachable) {
      assert(!hasOutParam(curr->value));
      replaceCurrent(curr->value);
      return;
    }
    assert(hasOutParam(curr->value) || curr->type == i64);
    switch (curr->op) {
      case ClzInt64:
      case CtzInt64:
      case PopcntInt64: goto err;
      case EqZInt64:               lowerEqZInt64(curr);     break;
      case ExtendSInt32: goto err;
      case ExtendUInt32:           lowerExtendUInt32(curr); break;
      case WrapInt64:              lowerWrapInt64(curr);    break;
      case TruncSFloat32ToInt64:
      case TruncUFloat32ToInt64:
      case TruncSFloat64ToInt64:
      case TruncUFloat64ToInt64:
      case ReinterpretFloat64:
      case ConvertSInt64ToFloat32:
      case ConvertSInt64ToFloat64:
      case ConvertUInt64ToFloat32:
      case ConvertUInt64ToFloat64:
      case ReinterpretInt64:
      err: default:
        std::cerr << "Unhandled unary operator: " << curr->op << std::endl;
        abort();
    }
  }

  Block* lowerAdd(Block* result, TempVar&& leftLow, TempVar&& leftHigh,
                  TempVar&& rightLow, TempVar&& rightHigh) {
    SetLocal* addLow = builder->makeSetLocal(
      leftHigh,
      builder->makeBinary(
        AddInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeGetLocal(rightLow, i32)
      )
    );
    SetLocal* addHigh = builder->makeSetLocal(
      rightHigh,
      builder->makeBinary(
        AddInt32,
        builder->makeGetLocal(leftHigh, i32),
        builder->makeGetLocal(rightHigh, i32)
      )
    );
    SetLocal* carryBit = builder->makeSetLocal(
      rightHigh,
      builder->makeBinary(
        AddInt32,
        builder->makeGetLocal(rightHigh, i32),
        builder->makeConst(Literal(int32_t(1)))
      )
    );
    If* checkOverflow = builder->makeIf(
      builder->makeBinary(
        LtUInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeGetLocal(rightLow, i32)
      ),
      carryBit
    );
    GetLocal* getLow = builder->makeGetLocal(leftHigh, i32);
    result = builder->blockify(result, addLow, addHigh, checkOverflow, getLow);
    setOutParam(result, std::move(rightHigh));
    return result;
  }

  Block* lowerMul(Block* result, TempVar&& leftLow, TempVar&& leftHigh,
                  TempVar&& rightLow, TempVar&& rightHigh) {
    // high bits = ll*rh + lh*rl + ll1*rl1 + (ll0*rl1)>>16 + (ll1*rl0)>>16
    // low bits = ll*rl
    TempVar leftLow0 = getTemp();
    TempVar leftLow1 = getTemp();
    TempVar rightLow0 = getTemp();
    TempVar rightLow1 = getTemp();
    SetLocal* setLL0 = builder->makeSetLocal(
      leftLow0,
      builder->makeBinary(
        AndInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeConst(Literal(int32_t(0xffff)))
      )
    );
    SetLocal* setLL1 = builder->makeSetLocal(
      leftLow1,
      builder->makeBinary(
        ShrUInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeConst(Literal(int32_t(16)))
      )
    );
    SetLocal* setRL0 = builder->makeSetLocal(
      rightLow0,
      builder->makeBinary(
        AndInt32,
        builder->makeGetLocal(rightLow, i32),
        builder->makeConst(Literal(int32_t(0xffff)))
      )
    );
    SetLocal* setRL1 = builder->makeSetLocal(
      rightLow1,
      builder->makeBinary(
        ShrUInt32,
        builder->makeGetLocal(rightLow, i32),
        builder->makeConst(Literal(int32_t(16)))
      )
    );
    SetLocal* setLLRH = builder->makeSetLocal(
      rightHigh,
      builder->makeBinary(
        MulInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeGetLocal(rightHigh, i32)
      )
    );
    auto addToHighBits = [&](Expression* expr) -> SetLocal* {
      return builder->makeSetLocal(
        rightHigh,
        builder->makeBinary(
          AddInt32,
          builder->makeGetLocal(rightHigh, i32),
          expr
        )
      );
    };
    SetLocal* addLHRL = addToHighBits(
      builder->makeBinary(
        MulInt32,
        builder->makeGetLocal(leftHigh, i32),
        builder->makeGetLocal(rightLow, i32)
      )
    );
    SetLocal* addLL1RL1 = addToHighBits(
      builder->makeBinary(
        MulInt32,
        builder->makeGetLocal(leftLow1, i32),
        builder->makeGetLocal(rightLow1, i32)
      )
    );
    SetLocal* addLL0RL1 = addToHighBits(
      builder->makeBinary(
        ShrUInt32,
        builder->makeBinary(
          MulInt32,
          builder->makeGetLocal(leftLow0, i32),
          builder->makeGetLocal(rightLow1, i32)
        ),
        builder->makeConst(Literal(int32_t(16)))
      )
    );
    SetLocal* addLL1RL0 = addToHighBits(
      builder->makeBinary(
        ShrUInt32,
        builder->makeBinary(
          MulInt32,
          builder->makeGetLocal(leftLow1, i32),
          builder->makeGetLocal(rightLow0, i32)
        ),
        builder->makeConst(Literal(int32_t(16)))
      )
    );
    Binary* getLow = builder->makeBinary(
      MulInt32,
      builder->makeGetLocal(leftLow, i32),
      builder->makeGetLocal(rightLow, i32)
    );
    result = builder->blockify(
      result,
      setLL0,
      setLL1,
      setRL0,
      setRL1,
      setLLRH,
      addLHRL,
      addLL1RL1,
      addLL0RL1,
      addLL1RL0,
      getLow
    );
    setOutParam(result, std::move(rightHigh));
    return result;
  }

  Block* lowerBitwise(BinaryOp op, Block* result, TempVar&& leftLow,
                      TempVar&& leftHigh, TempVar&& rightLow,
                      TempVar&& rightHigh) {
    BinaryOp op32;
    switch (op) {
      case AndInt64: op32 = AndInt32; break;
      case  OrInt64: op32 =  OrInt32; break;
      case XorInt64: op32 = XorInt32; break;
      default: abort();
    }
    result = builder->blockify(
      result,
      builder->makeSetLocal(
        rightHigh,
        builder->makeBinary(
          op32,
          builder->makeGetLocal(leftHigh, i32),
          builder->makeGetLocal(rightHigh, i32)
        )
      ),
      builder->makeBinary(
        op32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeGetLocal(rightLow, i32)
      )
    );
    setOutParam(result, std::move(rightHigh));
    return result;
  }

  Block* makeLargeShl(Index highBits, Index leftLow, Index shift) {
    return builder->blockify(
      builder->makeSetLocal(
        highBits,
        builder->makeBinary(
          ShlInt32,
          builder->makeGetLocal(leftLow, i32),
          builder->makeGetLocal(shift, i32)
        )
      ),
      builder->makeConst(Literal(int32_t(0)))
    );
  }

  Block* makeLargeShrU(Index highBits, Index leftHigh, Index shift) {
    return builder->blockify(
      builder->makeSetLocal(highBits, builder->makeConst(Literal(int32_t(0)))),
      builder->makeBinary(
        ShrUInt32,
        builder->makeGetLocal(leftHigh, i32),
        builder->makeGetLocal(shift, i32)
      )
    );
  }

  Block* makeSmallShl(Index highBits, Index leftLow, Index leftHigh,
                      Index shift, Binary* shiftMask, Binary* widthLessShift) {
    Binary* shiftedInBits = builder->makeBinary(
      AndInt32,
      shiftMask,
      builder->makeBinary(
        ShrUInt32,
        builder->makeGetLocal(leftLow, i32),
        widthLessShift
      )
    );
    Binary* shiftHigh = builder->makeBinary(
      ShlInt32,
      builder->makeGetLocal(leftHigh, i32),
      builder->makeGetLocal(shift, i32)
    );
    return builder->blockify(
      builder->makeSetLocal(
        highBits,
        builder->makeBinary(OrInt32, shiftedInBits, shiftHigh)
      ),
      builder->makeBinary(
        ShlInt32,
        builder->makeGetLocal(leftLow, i32),
        builder->makeGetLocal(shift, i32)
      )
    );
  }

  Block* makeSmallShrU(Index highBits, Index leftLow, Index leftHigh,
                       Index shift, Binary* shiftMask, Binary* widthLessShift) {
    Binary* shiftedInBits = builder->makeBinary(
      ShlInt32,
      builder->makeBinary(
        AndInt32,
        shiftMask,
        builder->makeGetLocal(leftHigh, i32)
      ),
      widthLessShift
    );
    Binary* shiftLow = builder->makeBinary(
      ShrUInt32,
      builder->makeGetLocal(leftLow, i32),
      builder->makeGetLocal(shift, i32)
    );
    return builder->blockify(
      builder->makeSetLocal(
        highBits,
        builder->makeBinary(
          ShrUInt32,
          builder->makeGetLocal(leftHigh, i32),
          builder->makeGetLocal(shift, i32)
        )
      ),
      builder->makeBinary(OrInt32, shiftedInBits, shiftLow)
    );
  }

  Block* lowerShU(BinaryOp op, Block* result, TempVar&& leftLow,
                  TempVar&& leftHigh, TempVar&& rightLow, TempVar&& rightHigh) {
    assert(op == ShlInt64 || op == ShrUInt64);
    // shift left lowered as:
    // if 32 <= rightLow % 64:
    //     high = leftLow << k; low = 0
    // else:
    //     high = (((1 << k) - 1) & (leftLow >> (32 - k))) | (leftHigh << k);
    //     low = leftLow << k
    // where k = shift % 32. shift right is similar.
    TempVar shift = getTemp();
    SetLocal* setShift = builder->makeSetLocal(
      shift,
      builder->makeBinary(
        AndInt32,
        builder->makeGetLocal(rightLow, i32),
        builder->makeConst(Literal(int32_t(32 - 1)))
      )
    );
    Binary* isLargeShift = builder->makeBinary(
      LeUInt32,
      builder->makeConst(Literal(int32_t(32))),
      builder->makeBinary(
        AndInt32,
        builder->makeGetLocal(rightLow, i32),
        builder->makeConst(Literal(int32_t(64 - 1)))
      )
    );
    Block* largeShiftBlock;
    switch (op) {
      case ShlInt64:
        largeShiftBlock = makeLargeShl(rightHigh, leftLow, shift); break;
      case ShrUInt64:
        largeShiftBlock = makeLargeShrU(rightHigh, leftHigh, shift); break;
      default: abort();
    }
    Binary* shiftMask = builder->makeBinary(
      SubInt32,
      builder->makeBinary(
        ShlInt32,
        builder->makeConst(Literal(int32_t(1))),
        builder->makeGetLocal(shift, i32)
      ),
      builder->makeConst(Literal(int32_t(1)))
    );
    Binary* widthLessShift = builder->makeBinary(
      SubInt32,
      builder->makeConst(Literal(int32_t(32))),
      builder->makeGetLocal(shift, i32)
    );
    Block* smallShiftBlock;
    switch(op) {
      case ShlInt64: {
        smallShiftBlock = makeSmallShl(rightHigh, leftLow, leftHigh,
                                       shift, shiftMask, widthLessShift);
        break;
      }
      case ShrUInt64: {
        smallShiftBlock = makeSmallShrU(rightHigh, leftLow, leftHigh,
                                        shift, shiftMask, widthLessShift);
        break;
      }
      default: abort();
    }
    If* ifLargeShift = builder->makeIf(
      isLargeShift,
      largeShiftBlock,
      smallShiftBlock
    );
    result = builder->blockify(result, setShift, ifLargeShift);
    setOutParam(result, std::move(rightHigh));
    return result;
  }

  Block* lowerEq(Block* result, TempVar&& leftLow, TempVar&& leftHigh,
                    TempVar&& rightLow, TempVar&& rightHigh) {
    return builder->blockify(
      result,
      builder->makeBinary(
        AndInt32,
        builder->makeBinary(
          EqInt32,
          builder->makeGetLocal(leftLow, i32),
          builder->makeGetLocal(rightLow, i32)
        ),
        builder->makeBinary(
          EqInt32,
          builder->makeGetLocal(leftHigh, i32),
          builder->makeGetLocal(rightHigh, i32)
        )
      )
    );
  }

  Block* lowerNe(Block* result, TempVar&& leftLow, TempVar&& leftHigh,
                 TempVar&& rightLow, TempVar&& rightHigh) {
    return builder->blockify(
      result,
      builder->makeBinary(
        OrInt32,
        builder->makeBinary(
          NeInt32,
          builder->makeGetLocal(leftLow, i32),
          builder->makeGetLocal(rightLow, i32)
        ),
        builder->makeBinary(
          NeInt32,
          builder->makeGetLocal(leftHigh, i32),
          builder->makeGetLocal(rightHigh, i32)
        )
      )
    );
  }

  Block* lowerUComp(BinaryOp op, Block* result, TempVar&& leftLow,
                    TempVar&& leftHigh, TempVar&& rightLow,
                    TempVar&& rightHigh) {
    BinaryOp highOp, lowOp;
    switch (op) {
      case LtUInt64: highOp = LtUInt32; lowOp = LtUInt32; break;
      case LeUInt64: highOp = LtUInt32; lowOp = LeUInt32; break;
      case GtUInt64: highOp = GtUInt32; lowOp = GtUInt32; break;
      case GeUInt64: highOp = GtUInt32; lowOp = GeUInt32; break;
      default: abort();
    }
    Binary* compHigh = builder->makeBinary(
      highOp,
      builder->makeGetLocal(leftHigh, i32),
      builder->makeGetLocal(rightHigh, i32)
    );
    Binary* eqHigh = builder->makeBinary(
      EqInt32,
      builder->makeGetLocal(leftHigh, i32),
      builder->makeGetLocal(rightHigh, i32)
    );
    Binary* compLow = builder->makeBinary(
      lowOp,
      builder->makeGetLocal(leftLow, i32),
      builder->makeGetLocal(rightLow, i32)
    );
    return builder->blockify(
      result,
      builder->makeBinary(
        OrInt32,
        compHigh,
        builder->makeBinary(AndInt32, eqHigh, compLow)
      )
    );
  }

  bool binaryNeedsLowering(BinaryOp op) {
    switch (op) {
      case AddInt64:
      case SubInt64:
      case MulInt64:
      case DivSInt64:
      case DivUInt64:
      case RemSInt64:
      case RemUInt64:
      case AndInt64:
      case OrInt64:
      case XorInt64:
      case ShlInt64:
      case ShrUInt64:
      case ShrSInt64:
      case RotLInt64:
      case RotRInt64:
      case EqInt64:
      case NeInt64:
      case LtSInt64:
      case LtUInt64:
      case LeSInt64:
      case LeUInt64:
      case GtSInt64:
      case GtUInt64:
      case GeSInt64:
      case GeUInt64: return true;
      default: return false;
    }
  }

  void visitBinary(Binary* curr) {
    if (!binaryNeedsLowering(curr->op)) return;
    if (!hasOutParam(curr->left)) {
      // left unreachable, replace self with left
      replaceCurrent(curr->left);
      if (hasOutParam(curr->right)) {
        // free temp var
        fetchOutParam(curr->right);
      }
      return;
    }
    if (!hasOutParam(curr->right)) {
      // right unreachable, replace self with left then right
      replaceCurrent(
        builder->blockify(builder->makeDrop(curr->left), curr->right)
      );
      // free temp var
      fetchOutParam(curr->left);
      return;
    }
    // left and right reachable, lower normally
    TempVar leftLow = getTemp();
    TempVar leftHigh = fetchOutParam(curr->left);
    TempVar rightLow = getTemp();
    TempVar rightHigh = fetchOutParam(curr->right);
    SetLocal* setRight = builder->makeSetLocal(rightLow, curr->right);
    SetLocal* setLeft = builder->makeSetLocal(leftLow, curr->left);
    Block* result = builder->blockify(setLeft, setRight);
    switch (curr->op) {
      case AddInt64: {
        replaceCurrent(
          lowerAdd(result, std::move(leftLow), std::move(leftHigh),
                   std::move(rightLow), std::move(rightHigh)));
        break;
      }
      case SubInt64: goto err;
      case MulInt64: {
        replaceCurrent(
          lowerMul(result, std::move(leftLow), std::move(leftHigh),
                   std::move(rightLow), std::move(rightHigh))
        );
        break;
      }
      case DivSInt64:
      case DivUInt64:
      case RemSInt64:
      case RemUInt64: goto err;
      case AndInt64:
      case OrInt64:
      case XorInt64: {
        replaceCurrent(
          lowerBitwise(curr->op, result, std::move(leftLow),
                       std::move(leftHigh), std::move(rightLow),
                       std::move(rightHigh))
        );
        break;
      }
      case ShlInt64:
      case ShrUInt64: {
        replaceCurrent(
          lowerShU(curr->op, result, std::move(leftLow), std::move(leftHigh),
                   std::move(rightLow), std::move(rightHigh))
        );
        break;
      }
      case ShrSInt64:
      case RotLInt64:
      case RotRInt64: goto err;
      case EqInt64: {
        replaceCurrent(
          lowerEq(result, std::move(leftLow), std::move(leftHigh),
                  std::move(rightLow), std::move(rightHigh))
        );
        break;
      }
      case NeInt64: {
        replaceCurrent(
          lowerNe(result, std::move(leftLow), std::move(leftHigh),
                  std::move(rightLow), std::move(rightHigh))
        );
        break;
      }
      case LtSInt64:
      case LeSInt64:
      case GtSInt64:
      case GeSInt64: goto err;
      case LtUInt64:
      case LeUInt64:
      case GtUInt64:
      case GeUInt64: {
        replaceCurrent(
          lowerUComp(curr->op, result, std::move(leftLow), std::move(leftHigh),
                     std::move(rightLow), std::move(rightHigh))
        );
        break;
      }
      err: default: {
        std::cerr << "Unhandled binary op " << curr->op << std::endl;
        abort();
      }
    }
  }

  void visitSelect(Select* curr) {
    visitBranching<Select>(curr);
  }

  void visitDrop(Drop* curr) {
    if (!hasOutParam(curr->value)) return;
    // free temp var
    fetchOutParam(curr->value);
  }

  void visitReturn(Return* curr) {
    if (!hasOutParam(curr->value)) return;
    TempVar lowBits = getTemp();
    TempVar highBits = fetchOutParam(curr->value);
    SetLocal* setLow = builder->makeSetLocal(lowBits, curr->value);
    SetGlobal* setHigh = builder->makeSetGlobal(
      highBitsGlobal,
      builder->makeGetLocal(highBits, i32)
    );
    curr->value = builder->makeGetLocal(lowBits, i32);
    Block* result = builder->blockify(setLow, setHigh, curr);
    replaceCurrent(result);
  }

private:
  std::unique_ptr<Builder> builder;
  std::unordered_map<Index, Index> indexMap;
  std::unordered_map<Expression*, TempVar> highBitVars;
  std::unordered_map<Name, TempVar> labelHighBitVars;
  std::vector<Index> freeTemps;
  Index nextTemp;

  TempVar getTemp() {
    Index ret;
    if (freeTemps.size() > 0) {
      ret = freeTemps.back();
      freeTemps.pop_back();
    } else {
      ret = nextTemp++;
    }
    return TempVar(ret, *this);
  }

  bool hasOutParam(Expression* e) {
    return highBitVars.find(e) != highBitVars.end();
  }

  void setOutParam(Expression* e, TempVar&& var) {
    highBitVars.emplace(e, std::move(var));
  }

  TempVar fetchOutParam(Expression* e) {
    auto outParamIt = highBitVars.find(e);
    assert(outParamIt != highBitVars.end());
    TempVar ret = std::move(outParamIt->second);
    highBitVars.erase(e);
    return ret;
  }
};

Name I64ToI32Lowering::highBitsGlobal("i64toi32_i32$HIGH_BITS");

Pass *createI64ToI32LoweringPass() {
  return new I64ToI32Lowering();
}

}
