// Copyright 2021 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "compiler/plugins/target/LLVMCPU/LinkerTool.h"
#include "iree/compiler/Utils/ToolUtils.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Support/FormatVariadic.h"

#define DEBUG_TYPE "llvm-linker"

namespace mlir::iree_compiler::IREE::HAL {

// Wasm linker using wasm-ld for producing WebAssembly binaries.
// wasm-ld behaves like traditional ELF linkers and uses similar flags.
//
// For details on the linking process and file formats, see:
// * https://lld.llvm.org/WebAssembly.html
// * https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md
//
// For more background on WebAssembly, see:
// * https://webassembly.org/
// * https://developer.mozilla.org/en-US/docs/WebAssembly
//
// When working with WebAssembly files, these projects are useful:
// * https://github.com/WebAssembly/wabt
// * https://github.com/bytecodealliance/wasmtime
//
// Use with `-iree-llvmcpu-target-triple=wasm32-unknown-unknown` (or
// equivalent). For SIMD support, also set
// `-iree-llvmcpu-target-cpu-features=+simd128`.
class WasmLinkerTool : public LinkerTool {
public:
  using LinkerTool::LinkerTool;

  std::string getWasmToolPath() const {
    // Always use the --iree-llvmcpu-wasm-linker-path flag when specified as
    // it's explicitly telling us what to use.
    if (!targetOptions.wasmLinkerPath.empty()) {
      return targetOptions.wasmLinkerPath;
    }

    // Allow overriding the automatic search with an environment variable.
    char *linkerPath = std::getenv("IREE_LLVM_WASM_LINKER_PATH");
    if (linkerPath) {
      return std::string(linkerPath);
    }

    // No explicit linker specified, search the environment (i.e. our own build
    // or install directories) for common tools.
    std::string toolPath = findToolFromExecutableDir(
        {"wasm-ld", "iree-lld", "lld", "ld.lld", "lld-link"});
    if (!toolPath.empty()) {
      return toolPath;
    }

    llvm::errs() << "No Wasm linker tool specified or discovered\n";
    return "";
  }

  LogicalResult
  configureModule(llvm::Module *llvmModule,
                  ArrayRef<llvm::Function *> exportedFuncs) override {
    // https://lld.llvm.org/WebAssembly.html#exports
    // Note: once we can set --shared this shouldn't be needed, since we set
    // default visibility on exported functions.
    for (auto func : exportedFuncs) {
      func->addFnAttr("wasm-export-name", func->getName());
    }

    return success();
  }

  std::optional<Artifacts>
  linkDynamicLibrary(StringRef libraryName,
                     ArrayRef<Artifact> objectFiles) override {
    Artifacts artifacts;

    // Create the wasm binary file name; if we only have a single input object
    // we can just reuse that.
    if (objectFiles.size() == 1) {
      artifacts.libraryFile =
          Artifact::createVariant(objectFiles.front().path, "wasm");
    } else {
      artifacts.libraryFile = Artifact::createTemporary(libraryName, "wasm");
    }
    artifacts.libraryFile.close();

    SmallVector<std::string, 8> flags = {
        getWasmToolPath(),

        // Forces LLD to act like wasm ld and produce WebAssembly files.
        // If not specified then lld tries to figure out what it is by progname
        // (ld, ld64, link, etc).
        // NOTE: must be first because lld sniffs argv[1]/argv[2].
        "-flavor wasm",

        // entry symbol not defined (pass --no-entry to suppress): _start
        "--no-entry",

        // Treat warnings as errors.
        "--fatal-warnings",

        // Generated a shared object containing position-independent-code.
        "--experimental-pic",
        "--shared",

        // Import [shared] memory from the environment.
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory#creating_a_shared_memory
        // TODO(scotttodd): Add a flag controlling these - some combination is
        //   required when using multithreading + SharedArrayBuffer, but they
        //   must be left off when running single threaded.
        // "--import-memory",
        // "--shared-memory",

        "-o " + artifacts.libraryFile.path,
    };

    // Strip debug information when not requested.
    if (!targetOptions.target.debugSymbols) {
      flags.push_back("--strip-debug");
    }

    // Link all input objects. Note that we are not linking whole-archive as
    // we want to allow dropping of unused codegen outputs.
    for (auto &objectFile : objectFiles) {
      flags.push_back(objectFile.path);
    }

    auto commandLine = llvm::join(flags, " ");
    if (failed(runLinkCommand(commandLine))) {
      return std::nullopt;
    }
    return artifacts;
  }
};

std::unique_ptr<LinkerTool>
createWasmLinkerTool(const llvm::Triple &targetTriple,
                     LLVMTargetOptions &targetOptions) {
  return std::make_unique<WasmLinkerTool>(targetTriple, targetOptions);
}

} // namespace mlir::iree_compiler::IREE::HAL
