//===- FuncToEmitC.cpp - Func to EmitC Patterns -----------------*- 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 implements patterns to convert the Func dialect to the EmitC
// dialect.
//
//===----------------------------------------------------------------------===//

#include "mlir/Conversion/FuncToEmitC/FuncToEmitC.h"

#include "mlir/Conversion/ConvertToEmitC/ToEmitCInterface.h"
#include "mlir/Dialect/EmitC/IR/EmitC.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/SymbolTable.h"
#include "mlir/Transforms/DialectConversion.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/LogicalResult.h"

using namespace mlir;

namespace {

//===----------------------------------------------------------------------===//
// Multi-return struct helpers
//===----------------------------------------------------------------------===//

// Looks up or creates an `emitc.class` named after `types` in the nearest
// enclosing symbol table of `op`, suitable for packing those types as plain
// struct fields (field0, field1, ...). If the class already exists it is
// verified to have exactly the right fields and no methods. Returns the
// corresponding !emitc.opaque<"struct ..."> type on success.
static FailureOr<emitc::OpaqueType>
getOrCreateMultiReturnType(ConversionPatternRewriter &rewriter, Location loc,
                           Operation *op, TypeRange types) {
  // Build the struct name from the types, e.g. "return_i32_i32". Each type is
  // printed and non-alphanumeric characters are replaced with '_'.
  std::string structName = "return";
  for (Type type : types) {
    std::string typeName;
    llvm::raw_string_ostream os(typeName);
    type.print(os);
    std::replace_if(
        typeName.begin(), typeName.end(),
        [](char c) { return !llvm::isAlnum(c); }, '_');
    structName += "_" + typeName;
  }

  // Find the enclosing symbol table and the direct child op within it that
  // contains `op`; the class will be inserted immediately before that child.
  Operation *symbolTableOp = SymbolTable::getNearestSymbolTable(op);
  Operation *insertBefore = op;
  while (insertBefore->getParentOp() != symbolTableOp)
    insertBefore = insertBefore->getParentOp();

  if (Operation *sym = SymbolTable::lookupSymbolIn(symbolTableOp, structName)) {
    auto classOp = dyn_cast<emitc::ClassOp>(sym);
    if (!classOp)
      return emitError(loc) << "symbol '" << structName
                            << "' exists but is not an emitc.class";

    if (classOp.getClassType() != emitc::ClassType::struct_)
      return emitError(loc)
             << "existing class '" << structName << "' is not a struct";

    SmallVector<emitc::FieldOp> fields;
    for (Operation &bodyOp : classOp.getBody().front()) {
      if (isa<emitc::FuncOp>(bodyOp))
        return emitError(loc) << "existing class '" << structName
                              << "' has methods; expected a plain struct";
      if (auto fieldOp = dyn_cast<emitc::FieldOp>(bodyOp))
        fields.push_back(fieldOp);
    }
    if (fields.size() != types.size())
      return emitError(loc) << "existing class '" << structName
                            << "' has wrong number of fields";
    for (auto [i, fieldOp] : llvm::enumerate(fields)) {
      if (fieldOp.getSymName() != "field" + std::to_string(i))
        return emitError(loc) << "existing class '" << structName
                              << "': unexpected field name at index " << i;
      if (fieldOp.getTypeAttr().getValue() != types[i])
        return emitError(loc) << "existing class '" << structName
                              << "': wrong type for field " << i;
    }
  } else {
    // Create the ClassOp before `insertBefore`, then restore the insertion
    // point.
    auto savedIP = rewriter.saveInsertionPoint();
    rewriter.setInsertionPoint(insertBefore);

    emitc::ClassOp classOp = emitc::ClassOp::create(rewriter, loc, structName,
                                                    /*final_specifier=*/false,
                                                    emitc::ClassType::struct_);
    rewriter.createBlock(&classOp.getBody());
    rewriter.setInsertionPointToStart(&classOp.getBody().front());

    for (auto [i, type] : llvm::enumerate(types)) {
      auto fieldName = rewriter.getStringAttr("field" + std::to_string(i));
      emitc::FieldOp::create(rewriter, loc, fieldName, TypeAttr::get(type),
                             nullptr);
    }

    rewriter.restoreInsertionPoint(savedIP);
  }
  return emitc::OpaqueType::get(rewriter.getContext(), "struct " + structName);
}

// Packs multiple SSA values into an emitc.class struct variable and loads the
// result as a single SSA value of the opaque struct type.
static Value packValuesIntoStruct(ConversionPatternRewriter &rewriter,
                                  Location loc, ValueRange values,
                                  emitc::OpaqueType structType) {
  MLIRContext *ctx = rewriter.getContext();
  auto noInit = emitc::OpaqueAttr::get(ctx, "");
  Value structLv =
      emitc::VariableOp::create(rewriter, loc,
                                emitc::LValueType::get(structType), noInit)
          .getResult();
  for (auto [i, val] : llvm::enumerate(values)) {
    Value fieldLv =
        emitc::MemberOp::create(
            rewriter, loc, emitc::LValueType::get(val.getType()),
            rewriter.getStringAttr("field" + std::to_string(i)), structLv)
            .getResult();
    emitc::AssignOp::create(rewriter, loc, fieldLv, val);
  }
  return emitc::LoadOp::create(rewriter, loc, structType, structLv).getResult();
}

/// Implement the interface to convert Func to EmitC.
struct FuncToEmitCDialectInterface : public ConvertToEmitCPatternInterface {
  FuncToEmitCDialectInterface(Dialect *dialect)
      : ConvertToEmitCPatternInterface(dialect) {}

  /// Hook for derived dialect interface to provide conversion patterns
  /// and mark dialect legal for the conversion target.
  void populateConvertToEmitCConversionPatterns(
      ConversionTarget &target, TypeConverter &typeConverter,
      RewritePatternSet &patterns, std::optional<bool> lowerToCpp) const final {
    populateFuncToEmitCPatterns(typeConverter, patterns,
                                lowerToCpp.value_or(true));
  }
};
} // namespace

void mlir::registerConvertFuncToEmitCInterface(DialectRegistry &registry) {
  registry.addExtension(+[](MLIRContext *ctx, func::FuncDialect *dialect) {
    dialect->addInterfaces<FuncToEmitCDialectInterface>();
  });
}

//===----------------------------------------------------------------------===//
// Conversion Patterns
//===----------------------------------------------------------------------===//

namespace {
class CallOpConversion final : public OpConversionPattern<func::CallOp> {
public:
  CallOpConversion(const TypeConverter &typeConverter, MLIRContext *ctx,
                   bool lowerToCpp)
      : OpConversionPattern<func::CallOp>(typeConverter, ctx),
        lowerToCpp(lowerToCpp) {}

  LogicalResult
  matchAndRewrite(func::CallOp callOp, OpAdaptor adaptor,
                  ConversionPatternRewriter &rewriter) const override {
    // Do not convert multiple-return functions if lowering target is Cpp.
    // The translator will emit the return values as an std::tuple.
    if (callOp.getNumResults() > 1 && lowerToCpp)
      return rewriter.notifyMatchFailure(
          callOp, "only functions with zero or one result can be converted");

    SmallVector<Type> convertedResultTypes;
    for (Type t : callOp.getResultTypes()) {
      Type resultType = getTypeConverter()->convertType(t);
      if (!resultType)
        return rewriter.notifyMatchFailure(callOp,
                                           "result type conversion failed");
      if (isa<emitc::ArrayType>(resultType))
        return rewriter.notifyMatchFailure(
            callOp, "function calls returning arrays are not supported");
      convertedResultTypes.push_back(resultType);
    }

    if (callOp.getNumResults() <= 1) {
      rewriter.replaceOpWithNewOp<emitc::CallOp>(callOp, convertedResultTypes,
                                                 adaptor.getOperands(),
                                                 callOp->getAttrs());
      return success();
    }

    // Multi-result call: determine the struct type.
    Location loc = callOp.getLoc();

    auto structType =
        getOrCreateMultiReturnType(rewriter, loc, callOp, convertedResultTypes);
    if (failed(structType))
      return rewriter.notifyMatchFailure(callOp,
                                         "incompatible multi-return struct");

    // Emit a call returning the packed struct.
    Value structVal =
        emitc::CallOp::create(rewriter, loc, callOp.getCalleeAttr(),
                              TypeRange{*structType}, adaptor.getOperands())
            .getResult(0);

    // Unpack struct fields to replace the original multiple results.
    SmallVector<Value> results;
    for (auto [i, result] : llvm::enumerate(callOp.getResults())) {
      if (result.use_empty()) {
        results.push_back(Value()); // No replacement needed.
        continue;
      }
      Type fieldType = convertedResultTypes[i];
      StringAttr fieldName =
          rewriter.getStringAttr("field" + std::to_string(i));
      Value fieldValue = emitc::MemberOp::create(rewriter, loc, fieldType,
                                                 fieldName, structVal)
                             .getResult();
      results.push_back(fieldValue);
    }

    rewriter.replaceOp(callOp, results);
    return success();
  }

private:
  bool lowerToCpp;
};

class FuncOpConversion final : public OpConversionPattern<func::FuncOp> {
public:
  FuncOpConversion(const TypeConverter &typeConverter, MLIRContext *ctx,
                   bool lowerToCpp)
      : OpConversionPattern<func::FuncOp>(typeConverter, ctx),
        lowerToCpp(lowerToCpp) {}

  LogicalResult
  matchAndRewrite(func::FuncOp funcOp, OpAdaptor adaptor,
                  ConversionPatternRewriter &rewriter) const override {
    FunctionType fnType = funcOp.getFunctionType();

    // Do not convert multiple-return functions if lowering target is Cpp.
    // The translator will emit the return values as an std::tuple.
    if (fnType.getNumResults() > 1 && lowerToCpp)
      return rewriter.notifyMatchFailure(
          funcOp, "only functions with zero or one result can be converted");

    TypeConverter::SignatureConversion signatureConverter(
        fnType.getNumInputs());
    for (const auto &argType : enumerate(fnType.getInputs())) {
      auto convertedType = getTypeConverter()->convertType(argType.value());
      if (!convertedType)
        return rewriter.notifyMatchFailure(funcOp,
                                           "argument type conversion failed");
      signatureConverter.addInputs(argType.index(), convertedType);
    }

    SmallVector<Type> convertedResultTypes;
    for (Type t : fnType.getResults()) {
      Type resultType = getTypeConverter()->convertType(t);
      if (!resultType)
        return rewriter.notifyMatchFailure(funcOp,
                                           "result type conversion failed");
      if (isa<emitc::ArrayType>(resultType))
        return rewriter.notifyMatchFailure(
            funcOp, "functions returning arrays are not supported");
      convertedResultTypes.push_back(resultType);
    }

    Type resultType;
    if (fnType.getNumResults() == 1) {
      resultType = convertedResultTypes[0];
    } else if (fnType.getNumResults() > 1) {
      auto structTypeOrErr = getOrCreateMultiReturnType(
          rewriter, funcOp.getLoc(), funcOp, convertedResultTypes);
      if (failed(structTypeOrErr))
        return rewriter.notifyMatchFailure(funcOp,
                                           "incompatible multi-return struct");
      resultType = *structTypeOrErr;
    }

    // Create the converted `emitc.func` op.
    emitc::FuncOp newFuncOp = emitc::FuncOp::create(
        rewriter, funcOp.getLoc(), funcOp.getName(),
        FunctionType::get(rewriter.getContext(),
                          signatureConverter.getConvertedTypes(),
                          resultType ? TypeRange(resultType) : TypeRange()));

    // Copy over all attributes other than the function name and type.
    for (const auto &namedAttr : funcOp->getAttrs()) {
      if (namedAttr.getName() != funcOp.getFunctionTypeAttrName() &&
          namedAttr.getName() != SymbolTable::getSymbolAttrName())
        newFuncOp->setAttr(namedAttr.getName(), namedAttr.getValue());
    }

    // Add `extern` to specifiers if `func.func` is declaration only.
    if (funcOp.isDeclaration()) {
      ArrayAttr specifiers = rewriter.getStrArrayAttr({"extern"});
      newFuncOp.setSpecifiersAttr(specifiers);
    }

    // Add `static` to specifiers if `func.func` is private but not a
    // declaration.
    if (funcOp.isPrivate() && !funcOp.isDeclaration()) {
      ArrayAttr specifiers = rewriter.getStrArrayAttr({"static"});
      newFuncOp.setSpecifiersAttr(specifiers);
    }

    if (!funcOp.isDeclaration()) {
      rewriter.inlineRegionBefore(funcOp.getBody(), newFuncOp.getBody(),
                                  newFuncOp.end());
      if (failed(rewriter.convertRegionTypes(
              &newFuncOp.getBody(), *getTypeConverter(), &signatureConverter)))
        return failure();
    }
    rewriter.eraseOp(funcOp);

    return success();
  }

private:
  bool lowerToCpp;
};

class ReturnOpConversion final : public OpConversionPattern<func::ReturnOp> {
public:
  ReturnOpConversion(const TypeConverter &typeConverter, MLIRContext *ctx,
                     bool lowerToCpp)
      : OpConversionPattern<func::ReturnOp>(typeConverter, ctx),
        lowerToCpp(lowerToCpp) {}

  LogicalResult
  matchAndRewrite(func::ReturnOp returnOp, OpAdaptor adaptor,
                  ConversionPatternRewriter &rewriter) const override {
    // Do not convert multiple-return functions if lowering target is Cpp.
    // The translator will emit the return values as an std::tuple.
    if (returnOp.getNumOperands() > 1 && lowerToCpp)
      return rewriter.notifyMatchFailure(
          returnOp, "only zero or one operand is supported");

    if (llvm::any_of(adaptor.getOperands(), [](Value operand) {
          return isa<emitc::ArrayType>(operand.getType());
        }))
      return rewriter.notifyMatchFailure(returnOp,
                                         "returning arrays is not supported");

    if (returnOp.getNumOperands() <= 1) {
      rewriter.replaceOpWithNewOp<emitc::ReturnOp>(
          returnOp,
          returnOp.getNumOperands() ? adaptor.getOperands()[0] : nullptr);
      return success();
    }

    // Multi-operand return: pack values into a struct.
    Location loc = returnOp.getLoc();

    auto structType = getOrCreateMultiReturnType(
        rewriter, loc, returnOp, adaptor.getOperands().getTypes());
    if (failed(structType))
      return rewriter.notifyMatchFailure(returnOp,
                                         "incompatible multi-return struct");

    Value structVal =
        packValuesIntoStruct(rewriter, loc, adaptor.getOperands(), *structType);
    rewriter.replaceOpWithNewOp<emitc::ReturnOp>(returnOp, structVal);
    return success();
  }

private:
  bool lowerToCpp;
};
} // namespace

//===----------------------------------------------------------------------===//
// Pattern population
//===----------------------------------------------------------------------===//

void mlir::populateFuncToEmitCPatterns(const TypeConverter &typeConverter,
                                       RewritePatternSet &patterns,
                                       bool lowerToCpp) {
  MLIRContext *ctx = patterns.getContext();

  patterns.add<CallOpConversion, FuncOpConversion, ReturnOpConversion>(
      typeConverter, ctx, lowerToCpp);
}
