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

//
// Hoists repeated constants to a local. A local.get takes 2 bytes
// in most cases, and if a const is larger than that, it may be
// better to store it to a local, then get it from that local.
//
// WARNING: this often shrinks code size, but can *increase* gzip
//          size. apparently having the constants in their proper
//          places lets them be compressed better, across
//          functions, etc. TODO investigate
// TODO: hoisting a zero does not even require an initial set!
// TODO: hoisting a float or double zero is especially beneficial as there
//       is no LEB compression for them, and no need for the set, so
//       each f32.const is 5 bytes and f64.const is 9 bytes, while it is
//       <= 1 byte to declare the local and 2-3 to use it!
//

#include "pass.h"
#include "support/insert_ordered.h"
#include "wasm-binary.h"
#include "wasm-builder.h"
#include "wasm.h"

namespace wasm {

// with fewer uses than this, it is never beneficial to hoist
static const Index MIN_USES = 2;

struct ConstHoisting : public WalkerPass<PostWalker<ConstHoisting>> {
  bool isFunctionParallel() override { return true; }

  std::unique_ptr<Pass> create() override {
    return std::make_unique<ConstHoisting>();
  }

  InsertOrderedMap<Literal, std::vector<Expression**>> uses;

  void visitConst(Const* curr) {
    uses[curr->value].push_back(getCurrentPointer());
  }

  void visitFunction(Function* curr) {
    std::vector<Expression*> prelude;
    for (auto& [value, vec] : uses) {
      auto num = vec.size();
      if (worthHoisting(value, num)) {
        prelude.push_back(hoist(vec));
      }
    }
    if (!prelude.empty()) {
      Builder builder(*getModule());
      // merge-blocks can optimize this into a single block later in most cases
      curr->body = builder.makeSequence(builder.makeBlock(prelude), curr->body);
    }
  }

private:
  bool worthHoisting(Literal value, Index num) {
    if (num < MIN_USES) {
      return false;
    }
    // measure the size of the constant
    Index size = 0;
    TODO_SINGLE_COMPOUND(value.type);
    switch (value.type.getBasic()) {
      case Type::i32: {
        size = getWrittenSize(S32LEB(value.geti32()));
        break;
      }
      case Type::i64: {
        size = getWrittenSize(S64LEB(value.geti64()));
        break;
      }
      case Type::f32:
      case Type::f64: {
        size = value.type.getByteSize();
        break;
      }
      // not implemented yet
      case Type::v128:
        return false;
      case Type::none:
      case Type::unreachable:
        WASM_UNREACHABLE("unexpected type");
    }
    // compute the benefit, of replacing the uses with
    // one use + a set and then a get for each use
    // doing the algebra, the criterion here is when
    //   size > 2(1+num)/(num-1)
    // or
    //   num > (size+2)/(size-2)
    auto before = num * size;
    auto after = size + 2 /* local.set */ + (2 /* local.get */ * num);
    return after < before;
  }

  template<typename T> Index getWrittenSize(const T& thing) {
    BufferWithRandomAccess buffer;
    buffer << thing;
    return buffer.size();
  }

  // replace all the uses with gets, for a local set at the top. returns
  // the set.
  Expression* hoist(std::vector<Expression**>& vec) {
    auto type = (*(vec[0]))->type;
    Builder builder(*getModule());
    auto temp = builder.addVar(getFunction(), type);
    auto* ret = builder.makeLocalSet(temp, *(vec[0]));
    for (auto item : vec) {
      *item = builder.makeLocalGet(temp, type);
    }
    return ret;
  }
};

Pass* createConstHoistingPass() { return new ConstHoisting(); }

} // namespace wasm
