//===- Diagnostics.h - MLIR Diagnostics -------------------------*- C++ -*-===//
//
// Part of the LLVM Project, 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
//
//===----------------------------------------------------------------------===//
//
// This file defines utilities for emitting diagnostics.
//
//===----------------------------------------------------------------------===//

#ifndef MLIR_IR_DIAGNOSTICS_H
#define MLIR_IR_DIAGNOSTICS_H

#include "mlir/IR/Location.h"
#include <functional>
#include <optional>

namespace llvm {
class MemoryBuffer;
class SMLoc;
class SourceMgr;
} // namespace llvm

namespace mlir {
class DiagnosticEngine;
struct LogicalResult;
class MLIRContext;
class Operation;
class OperationName;
class OpPrintingFlags;
class Type;
class Value;

namespace detail {
struct DiagnosticEngineImpl;
} // namespace detail

/// Defines the different supported severity of a diagnostic.
enum class DiagnosticSeverity {
  Note,
  Warning,
  Error,
  Remark,
};

//===----------------------------------------------------------------------===//
// DiagnosticArgument
//===----------------------------------------------------------------------===//

/// A variant type that holds a single argument for a diagnostic.
class DiagnosticArgument {
public:
  /// Note: The constructors below are only exposed due to problems accessing
  /// constructors from type traits, they should not be used directly by users.
  // Construct from an Attribute.
  explicit DiagnosticArgument(Attribute attr);
  // Construct from a floating point number.
  explicit DiagnosticArgument(double val)
      : kind(DiagnosticArgumentKind::Double), doubleVal(val) {}
  explicit DiagnosticArgument(float val) : DiagnosticArgument(double(val)) {}
  // Construct from a signed integer.
  template <typename T>
  explicit DiagnosticArgument(
      T val, std::enable_if_t<std::is_signed<T>::value &&
                              std::numeric_limits<T>::is_integer &&
                              sizeof(T) <= sizeof(int64_t)> * = nullptr)
      : kind(DiagnosticArgumentKind::Integer), opaqueVal(int64_t(val)) {}
  // Construct from an unsigned integer.
  template <typename T>
  explicit DiagnosticArgument(
      T val, std::enable_if_t<std::is_unsigned<T>::value &&
                              std::numeric_limits<T>::is_integer &&
                              sizeof(T) <= sizeof(uint64_t)> * = nullptr)
      : kind(DiagnosticArgumentKind::Unsigned), opaqueVal(uint64_t(val)) {}
  // Construct from a string reference.
  explicit DiagnosticArgument(StringRef val)
      : kind(DiagnosticArgumentKind::String), stringVal(val) {}
  // Construct from a Type.
  explicit DiagnosticArgument(Type val);

  /// Enum that represents the different kinds of diagnostic arguments
  /// supported.
  enum class DiagnosticArgumentKind {
    Attribute,
    Double,
    Integer,
    String,
    Type,
    Unsigned,
  };

  /// Outputs this argument to a stream.
  void print(raw_ostream &os) const;

  /// Returns the kind of this argument.
  DiagnosticArgumentKind getKind() const { return kind; }

  /// Returns this argument as an Attribute.
  Attribute getAsAttribute() const;

  /// Returns this argument as a double.
  double getAsDouble() const {
    assert(getKind() == DiagnosticArgumentKind::Double);
    return doubleVal;
  }

  /// Returns this argument as a signed integer.
  int64_t getAsInteger() const {
    assert(getKind() == DiagnosticArgumentKind::Integer);
    return static_cast<int64_t>(opaqueVal);
  }

  /// Returns this argument as a string.
  StringRef getAsString() const {
    assert(getKind() == DiagnosticArgumentKind::String);
    return stringVal;
  }

  /// Returns this argument as a Type.
  Type getAsType() const;

  /// Returns this argument as an unsigned integer.
  uint64_t getAsUnsigned() const {
    assert(getKind() == DiagnosticArgumentKind::Unsigned);
    return static_cast<uint64_t>(opaqueVal);
  }

private:
  friend class Diagnostic;

  /// The kind of this argument.
  DiagnosticArgumentKind kind;

  /// The value of this argument.
  union {
    double doubleVal;
    intptr_t opaqueVal;
    StringRef stringVal;
  };
};

inline raw_ostream &operator<<(raw_ostream &os, const DiagnosticArgument &arg) {
  arg.print(os);
  return os;
}

//===----------------------------------------------------------------------===//
// Diagnostic
//===----------------------------------------------------------------------===//

/// This class contains all of the information necessary to report a diagnostic
/// to the DiagnosticEngine. It should generally not be constructed directly,
/// and instead used transitively via InFlightDiagnostic.
class Diagnostic {
  using NoteVector = std::vector<std::unique_ptr<Diagnostic>>;

public:
  Diagnostic(Location loc, DiagnosticSeverity severity)
      : loc(loc), severity(severity) {}
  Diagnostic(Diagnostic &&) = default;
  Diagnostic &operator=(Diagnostic &&) = default;

  /// Returns the severity of this diagnostic.
  DiagnosticSeverity getSeverity() const { return severity; }

  /// Returns the source location for this diagnostic.
  Location getLocation() const { return loc; }

  /// Returns the current list of diagnostic arguments.
  MutableArrayRef<DiagnosticArgument> getArguments() { return arguments; }
  ArrayRef<DiagnosticArgument> getArguments() const { return arguments; }

  /// Stream operator for inserting new diagnostic arguments.
  template <typename Arg>
  std::enable_if_t<!std::is_convertible<Arg, StringRef>::value &&
                       std::is_constructible<DiagnosticArgument, Arg>::value,
                   Diagnostic &>
  operator<<(Arg &&val) {
    arguments.push_back(DiagnosticArgument(std::forward<Arg>(val)));
    return *this;
  }
  Diagnostic &operator<<(StringAttr val);

  /// Stream in a string literal.
  Diagnostic &operator<<(const char *val) {
    arguments.push_back(DiagnosticArgument(val));
    return *this;
  }

  /// Stream in a Twine argument.
  Diagnostic &operator<<(char val);
  Diagnostic &operator<<(const Twine &val);
  Diagnostic &operator<<(Twine &&val);

  /// Stream in an OperationName.
  Diagnostic &operator<<(OperationName val);

  /// Stream in an Operation.
  Diagnostic &operator<<(Operation &op);
  Diagnostic &operator<<(Operation *op) { return *this << *op; }
  /// Append an operation with the given printing flags.
  Diagnostic &appendOp(Operation &op, const OpPrintingFlags &flags);

  /// Stream in a Value.
  Diagnostic &operator<<(Value val);

  /// Stream in a range.
  template <typename T, typename ValueT = llvm::detail::ValueOfRange<T>>
  std::enable_if_t<!std::is_constructible<DiagnosticArgument, T>::value,
                   Diagnostic &>
  operator<<(T &&range) {
    return appendRange(range);
  }

  /// Append a range to the diagnostic. The default delimiter between elements
  /// is ','.
  template <typename T>
  Diagnostic &appendRange(const T &c, const char *delim = ", ") {
    llvm::interleave(
        c, [this](const auto &a) { *this << a; }, [&]() { *this << delim; });
    return *this;
  }

  /// Append arguments to the diagnostic.
  template <typename Arg1, typename Arg2, typename... Args>
  Diagnostic &append(Arg1 &&arg1, Arg2 &&arg2, Args &&...args) {
    append(std::forward<Arg1>(arg1));
    return append(std::forward<Arg2>(arg2), std::forward<Args>(args)...);
  }
  /// Append one argument to the diagnostic.
  template <typename Arg>
  Diagnostic &append(Arg &&arg) {
    *this << std::forward<Arg>(arg);
    return *this;
  }

  /// Outputs this diagnostic to a stream.
  void print(raw_ostream &os) const;

  /// Converts the diagnostic to a string.
  std::string str() const;

  /// Attaches a note to this diagnostic. A new location may be optionally
  /// provided, if not, then the location defaults to the one specified for this
  /// diagnostic. Notes may not be attached to other notes.
  Diagnostic &attachNote(std::optional<Location> noteLoc = std::nullopt);

  using note_iterator = llvm::pointee_iterator<NoteVector::iterator>;
  using const_note_iterator =
      llvm::pointee_iterator<NoteVector::const_iterator>;

  /// Returns the notes held by this diagnostic.
  iterator_range<note_iterator> getNotes() {
    return llvm::make_pointee_range(notes);
  }
  iterator_range<const_note_iterator> getNotes() const {
    return llvm::make_pointee_range(notes);
  }

  /// Allow a diagnostic to be converted to 'failure'.
  operator LogicalResult() const;

  /// Allow a diagnostic to be converted to 'failure'.
  operator ParseResult() const { return ParseResult(LogicalResult(*this)); }

  /// Allow a diagnostic to be converted to FailureOr<T>. Always results in
  /// 'failure' because this cast cannot possibly return an object of 'T'.
  template <typename T>
  operator FailureOr<T>() const {
    return failure();
  }

private:
  Diagnostic(const Diagnostic &rhs) = delete;
  Diagnostic &operator=(const Diagnostic &rhs) = delete;

  /// The source location.
  Location loc;

  /// The severity of this diagnostic.
  DiagnosticSeverity severity;

  /// The current list of arguments.
  SmallVector<DiagnosticArgument, 4> arguments;

  /// A list of string values used as arguments. This is used to guarantee the
  /// liveness of non-constant strings used in diagnostics.
  std::vector<std::unique_ptr<char[]>> strings;

  /// A list of attached notes.
  NoteVector notes;
};

inline raw_ostream &operator<<(raw_ostream &os, const Diagnostic &diag) {
  diag.print(os);
  return os;
}

//===----------------------------------------------------------------------===//
// InFlightDiagnostic
//===----------------------------------------------------------------------===//

/// This class represents a diagnostic that is inflight and set to be reported.
/// This allows for last minute modifications of the diagnostic before it is
/// emitted by a DiagnosticEngine.
class InFlightDiagnostic {
public:
  InFlightDiagnostic() = default;
  InFlightDiagnostic(InFlightDiagnostic &&rhs)
      : owner(rhs.owner), impl(std::move(rhs.impl)) {
    // Reset the rhs diagnostic.
    rhs.impl.reset();
    rhs.abandon();
  }
  ~InFlightDiagnostic() {
    if (isInFlight())
      report();
  }

  /// Stream operator for new diagnostic arguments.
  template <typename Arg>
  InFlightDiagnostic &operator<<(Arg &&arg) & {
    return append(std::forward<Arg>(arg));
  }
  template <typename Arg>
  InFlightDiagnostic &&operator<<(Arg &&arg) && {
    return std::move(append(std::forward<Arg>(arg)));
  }

  /// Append arguments to the diagnostic.
  template <typename... Args>
  InFlightDiagnostic &append(Args &&...args) & {
    assert(isActive() && "diagnostic not active");
    if (isInFlight())
      impl->append(std::forward<Args>(args)...);
    return *this;
  }
  template <typename... Args>
  InFlightDiagnostic &&append(Args &&...args) && {
    return std::move(append(std::forward<Args>(args)...));
  }

  /// Attaches a note to this diagnostic.
  Diagnostic &attachNote(std::optional<Location> noteLoc = std::nullopt) {
    assert(isActive() && "diagnostic not active");
    return impl->attachNote(noteLoc);
  }

  /// Returns the underlying diagnostic or nullptr if this diagnostic isn't
  /// active.
  Diagnostic *getUnderlyingDiagnostic() { return impl ? &*impl : nullptr; }

  /// Reports the diagnostic to the engine.
  void report();

  /// Abandons this diagnostic so that it will no longer be reported.
  void abandon();

  /// Allow an inflight diagnostic to be converted to 'failure', otherwise
  /// 'success' if this is an empty diagnostic.
  operator LogicalResult() const;

  /// Allow an inflight diagnostic to be converted to 'failure', otherwise
  /// 'success' if this is an empty diagnostic.
  operator ParseResult() const { return ParseResult(LogicalResult(*this)); }

  /// Allow an inflight diagnostic to be converted to FailureOr<T>. Always
  /// results in 'failure' because this cast cannot possibly return an object of
  /// 'T'.
  template <typename T>
  operator FailureOr<T>() const {
    return failure();
  }

private:
  InFlightDiagnostic &operator=(const InFlightDiagnostic &) = delete;
  InFlightDiagnostic &operator=(InFlightDiagnostic &&) = delete;
  InFlightDiagnostic(DiagnosticEngine *owner, Diagnostic &&rhs)
      : owner(owner), impl(std::move(rhs)) {}

  /// Returns true if the diagnostic is still active, i.e. it has a live
  /// diagnostic.
  bool isActive() const { return impl.has_value(); }

  /// Returns true if the diagnostic is still in flight to be reported.
  bool isInFlight() const { return owner; }

  // Allow access to the constructor.
  friend DiagnosticEngine;

  /// The engine that this diagnostic is to report to.
  DiagnosticEngine *owner = nullptr;

  /// The raw diagnostic that is inflight to be reported.
  std::optional<Diagnostic> impl;
};

//===----------------------------------------------------------------------===//
// DiagnosticEngine
//===----------------------------------------------------------------------===//

/// This class is the main interface for diagnostics. The DiagnosticEngine
/// manages the registration of diagnostic handlers as well as the core API for
/// diagnostic emission. This class should not be constructed directly, but
/// instead interfaced with via an MLIRContext instance.
class DiagnosticEngine {
public:
  ~DiagnosticEngine();

  // Diagnostic handler registration and use. MLIR supports the ability for the
  // IR to carry arbitrary metadata about operation location information. If a
  // problem is detected by the compiler, it can invoke the emitError /
  // emitWarning / emitRemark method on an Operation and have it get reported
  // through this interface.
  //
  // Tools using MLIR are encouraged to register error handlers and define a
  // schema for their location information.  If they don't, then warnings and
  // notes will be dropped and errors will be emitted to errs.

  /// The handler type for MLIR diagnostics. This function takes a diagnostic as
  /// input, and returns success if the handler has fully processed this
  /// diagnostic. Returns failure otherwise.
  using HandlerTy = llvm::unique_function<LogicalResult(Diagnostic &)>;

  /// A handle to a specific registered handler object.
  using HandlerID = uint64_t;

  /// Register a new handler for diagnostics to the engine. Diagnostics are
  /// process by handlers in stack-like order, meaning that the last added
  /// handlers will process diagnostics first. This function returns a unique
  /// identifier for the registered handler, which can be used to unregister
  /// this handler at a later time.
  HandlerID registerHandler(HandlerTy handler);

  /// Set the diagnostic handler with a function that returns void. This is a
  /// convenient wrapper for handlers that always completely process the given
  /// diagnostic.
  template <typename FuncTy, typename RetT = decltype(std::declval<FuncTy>()(
                                 std::declval<Diagnostic &>()))>
  std::enable_if_t<std::is_same<RetT, void>::value, HandlerID>
  registerHandler(FuncTy &&handler) {
    return registerHandler([=](Diagnostic &diag) {
      handler(diag);
      return success();
    });
  }

  /// Erase the registered diagnostic handler with the given identifier.
  void eraseHandler(HandlerID id);

  /// Create a new inflight diagnostic with the given location and severity.
  InFlightDiagnostic emit(Location loc, DiagnosticSeverity severity) {
    assert(severity != DiagnosticSeverity::Note &&
           "notes should not be emitted directly");
    return InFlightDiagnostic(this, Diagnostic(loc, severity));
  }

  /// Emit a diagnostic using the registered issue handler if present, or with
  /// the default behavior if not. The diagnostic instance is consumed in the
  /// process.
  void emit(Diagnostic &&diag);

private:
  friend class MLIRContextImpl;
  DiagnosticEngine();

  /// The internal implementation of the DiagnosticEngine.
  std::unique_ptr<detail::DiagnosticEngineImpl> impl;
};

/// Utility method to emit an error message using this location.
InFlightDiagnostic emitError(Location loc);
InFlightDiagnostic emitError(Location loc, const Twine &message);

/// Utility method to emit a warning message using this location.
InFlightDiagnostic emitWarning(Location loc);
InFlightDiagnostic emitWarning(Location loc, const Twine &message);

/// Utility method to emit a remark message using this location.
InFlightDiagnostic emitRemark(Location loc);
InFlightDiagnostic emitRemark(Location loc, const Twine &message);

/// Overloads of the above emission functions that take an optionally null
/// location. If the location is null, no diagnostic is emitted and a failure is
/// returned. Given that the provided location may be null, these methods take
/// the diagnostic arguments directly instead of relying on the returned
/// InFlightDiagnostic.
template <typename... Args>
LogicalResult emitOptionalError(std::optional<Location> loc, Args &&...args) {
  if (loc)
    return emitError(*loc).append(std::forward<Args>(args)...);
  return failure();
}
template <typename... Args>
LogicalResult emitOptionalWarning(std::optional<Location> loc, Args &&...args) {
  if (loc)
    return emitWarning(*loc).append(std::forward<Args>(args)...);
  return failure();
}
template <typename... Args>
LogicalResult emitOptionalRemark(std::optional<Location> loc, Args &&...args) {
  if (loc)
    return emitRemark(*loc).append(std::forward<Args>(args)...);
  return failure();
}

//===----------------------------------------------------------------------===//
// ScopedDiagnosticHandler
//===----------------------------------------------------------------------===//

/// This diagnostic handler is a simple RAII class that registers and erases a
/// diagnostic handler on a given context. This class can be either be used
/// directly, or in conjunction with a derived diagnostic handler.
class ScopedDiagnosticHandler {
public:
  explicit ScopedDiagnosticHandler(MLIRContext *ctx) : handlerID(0), ctx(ctx) {}
  template <typename FuncTy>
  ScopedDiagnosticHandler(MLIRContext *ctx, FuncTy &&handler)
      : handlerID(0), ctx(ctx) {
    setHandler(std::forward<FuncTy>(handler));
  }
  ~ScopedDiagnosticHandler();

protected:
  /// Set the handler to manage via RAII.
  template <typename FuncTy>
  void setHandler(FuncTy &&handler) {
    auto &diagEngine = ctx->getDiagEngine();
    if (handlerID)
      diagEngine.eraseHandler(handlerID);
    handlerID = diagEngine.registerHandler(std::forward<FuncTy>(handler));
  }

private:
  /// The unique id for the scoped handler.
  DiagnosticEngine::HandlerID handlerID;

  /// The context to erase the handler from.
  MLIRContext *ctx;
};

//===----------------------------------------------------------------------===//
// SourceMgrDiagnosticHandler
//===----------------------------------------------------------------------===//

namespace detail {
struct SourceMgrDiagnosticHandlerImpl;
} // namespace detail

/// This class is a utility diagnostic handler for use with llvm::SourceMgr.
class SourceMgrDiagnosticHandler : public ScopedDiagnosticHandler {
public:
  /// This type represents a functor used to filter out locations when printing
  /// a diagnostic. It should return true if the provided location is okay to
  /// display, false otherwise. If all locations in a diagnostic are filtered
  /// out, the first location is used as the sole location. When deciding
  /// whether or not to filter a location, this function should not recurse into
  /// any nested location. This recursion is handled automatically by the
  /// caller.
  using ShouldShowLocFn = llvm::unique_function<bool(Location)>;

  SourceMgrDiagnosticHandler(llvm::SourceMgr &mgr, MLIRContext *ctx,
                             raw_ostream &os,
                             ShouldShowLocFn &&shouldShowLocFn = {});
  SourceMgrDiagnosticHandler(llvm::SourceMgr &mgr, MLIRContext *ctx,
                             ShouldShowLocFn &&shouldShowLocFn = {});
  ~SourceMgrDiagnosticHandler();

  /// Emit the given diagnostic information with the held source manager.
  void emitDiagnostic(Location loc, Twine message, DiagnosticSeverity kind,
                      bool displaySourceLine = true);

protected:
  /// Emit the given diagnostic with the held source manager.
  void emitDiagnostic(Diagnostic &diag);

  /// Get a memory buffer for the given file, or nullptr if no file is
  /// available.
  const llvm::MemoryBuffer *getBufferForFile(StringRef filename);

  /// The source manager that we are wrapping.
  llvm::SourceMgr &mgr;

  /// The output stream to use when printing diagnostics.
  raw_ostream &os;

  /// A functor used when determining if a location for a diagnostic should be
  /// shown. If null, all locations should be shown.
  ShouldShowLocFn shouldShowLocFn;

private:
  /// Convert a location into the given memory buffer into an SMLoc.
  SMLoc convertLocToSMLoc(FileLineColLoc loc);

  /// Given a location, returns the first nested location (including 'loc') that
  /// can be shown to the user.
  std::optional<Location> findLocToShow(Location loc);

  /// The maximum depth that a call stack will be printed.
  /// TODO: This should be a tunable flag.
  unsigned callStackLimit = 10;

  std::unique_ptr<detail::SourceMgrDiagnosticHandlerImpl> impl;
};

//===----------------------------------------------------------------------===//
// SourceMgrDiagnosticVerifierHandler
//===----------------------------------------------------------------------===//

namespace detail {
struct SourceMgrDiagnosticVerifierHandlerImpl;
} // namespace detail

/// This class is a utility diagnostic handler for use with llvm::SourceMgr that
/// verifies that emitted diagnostics match 'expected-*' lines on the
/// corresponding line of the source file.
class SourceMgrDiagnosticVerifierHandler : public SourceMgrDiagnosticHandler {
public:
  SourceMgrDiagnosticVerifierHandler(llvm::SourceMgr &srcMgr, MLIRContext *ctx,
                                     raw_ostream &out);
  SourceMgrDiagnosticVerifierHandler(llvm::SourceMgr &srcMgr, MLIRContext *ctx);
  ~SourceMgrDiagnosticVerifierHandler();

  /// Returns the status of the handler and verifies that all expected
  /// diagnostics were emitted. This return success if all diagnostics were
  /// verified correctly, failure otherwise.
  LogicalResult verify();

private:
  /// Process a single diagnostic.
  void process(Diagnostic &diag);

  /// Process a FileLineColLoc diagnostic.
  void process(FileLineColLoc loc, StringRef msg, DiagnosticSeverity kind);

  std::unique_ptr<detail::SourceMgrDiagnosticVerifierHandlerImpl> impl;
};

//===----------------------------------------------------------------------===//
// ParallelDiagnosticHandler
//===----------------------------------------------------------------------===//

namespace detail {
struct ParallelDiagnosticHandlerImpl;
} // namespace detail

/// This class is a utility diagnostic handler for use when multi-threading some
/// part of the compiler where diagnostics may be emitted. This handler ensures
/// a deterministic ordering to the emitted diagnostics that mirrors that of a
/// single-threaded compilation.
class ParallelDiagnosticHandler {
public:
  ParallelDiagnosticHandler(MLIRContext *ctx);
  ~ParallelDiagnosticHandler();

  /// Set the order id for the current thread. This is required to be set by
  /// each thread that will be emitting diagnostics to this handler. The orderID
  /// corresponds to the order in which diagnostics would be emitted when
  /// executing synchronously. For example, if we were processing a list
  /// of operations [a, b, c] on a single-thread. Diagnostics emitted while
  /// processing operation 'a' would be emitted before those for 'b' or 'c'.
  /// This corresponds 1-1 with the 'orderID'. The thread that is processing 'a'
  /// should set the orderID to '0'; the thread processing 'b' should set it to
  /// '1'; and so on and so forth. This provides a way for the handler to
  /// deterministically order the diagnostics that it receives given the thread
  /// that it is receiving on.
  void setOrderIDForThread(size_t orderID);

  /// Remove the order id for the current thread. This removes the thread from
  /// diagnostics tracking.
  void eraseOrderIDForThread();

private:
  std::unique_ptr<detail::ParallelDiagnosticHandlerImpl> impl;
};
} // namespace mlir

#endif
