From babe1f2d61fb751dd05769d2ead3663f34a064c7 Mon Sep 17 00:00:00 2001 From: Martin Erhart Date: Wed, 8 Jan 2025 13:17:07 +0100 Subject: [PATCH] [RTG] Add ISA assembly emission pass --- .../circt/Dialect/RTG/Transforms/RTGPasses.td | 32 +++ lib/Dialect/RTG/Transforms/CMakeLists.txt | 2 + .../RTG/Transforms/EmitRTGISAAssemblyPass.cpp | 249 ++++++++++++++++++ .../emit-rtg-isa-assembly-split-invalid.mlir | 6 + .../emit-rtg-isa-assembly-split.mlir | 24 ++ .../RTG/Transform/emit-rtg-isa-assembly.mlir | 58 ++++ .../RTG/Transform/unsupported-instr.txt | 1 + 7 files changed, 372 insertions(+) create mode 100644 lib/Dialect/RTG/Transforms/EmitRTGISAAssemblyPass.cpp create mode 100644 test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split-invalid.mlir create mode 100644 test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split.mlir create mode 100644 test/Dialect/RTG/Transform/emit-rtg-isa-assembly.mlir create mode 100644 test/Dialect/RTG/Transform/unsupported-instr.txt diff --git a/include/circt/Dialect/RTG/Transforms/RTGPasses.td b/include/circt/Dialect/RTG/Transforms/RTGPasses.td index 6eff4b5be034..f36d42ede531 100644 --- a/include/circt/Dialect/RTG/Transforms/RTGPasses.td +++ b/include/circt/Dialect/RTG/Transforms/RTGPasses.td @@ -32,4 +32,36 @@ def ElaborationPass : Pass<"rtg-elaborate", "mlir::ModuleOp"> { let dependentDialects = ["mlir::arith::ArithDialect"]; } +def EmitRTGISAAssemblyPass : Pass<"rtg-emit-isa-assembly", "mlir::ModuleOp"> { + let summary = "Elaborate the contexts of RTG"; + let description = [{ + Emits all 'rtg.test's in the IR in a format understood by assemblers. + + There are two options to specify lists of instructions that are not + supported by the assembler. For instructions in any of those lists, this + pass will emit the equivalent binary representation. + + This pass operates on the `InstructionOpInterface` and folds constant-like + operations to support downstream dialects. + }]; + + let options = [ + Option<"splitOutput", "split-output", "bool", /*default=*/"false", + "If 'true' emits one file per 'rtg.test' in the IR. The name of the " + "file matches the test name and is placed in 'path'. Otherwise, path " + "is interpreted as the full file path including filename.">, + Option<"path", "path", "std::string", /*default=*/"", + "The directory or file path in which the output files should be " + "created. If empty is is emitted to stderr (not allowed if " + "'split-output' is set to 'true')">, + Option<"unsupportedInstructionsFile", "unsupported-instructions-file", + "std::string", /*default=*/"", + "An absolute path to a file with a list of instruction names not " + "supported by the assembler.">, + ListOption<"unsupportedInstructions", "unsupported-instructions", + "std::string", + "A list of ISA instruction names not supported by the assembler.">, + ]; +} + #endif // CIRCT_DIALECT_RTG_TRANSFORMS_RTGPASSES_TD diff --git a/lib/Dialect/RTG/Transforms/CMakeLists.txt b/lib/Dialect/RTG/Transforms/CMakeLists.txt index 70dcd46cb5f9..9e4724e478d2 100644 --- a/lib/Dialect/RTG/Transforms/CMakeLists.txt +++ b/lib/Dialect/RTG/Transforms/CMakeLists.txt @@ -1,5 +1,6 @@ add_circt_dialect_library(CIRCTRTGTransforms ElaborationPass.cpp + EmitRTGISAAssemblyPass.cpp DEPENDS CIRCTRTGTransformsIncGen @@ -9,6 +10,7 @@ add_circt_dialect_library(CIRCTRTGTransforms LINK_LIBS PRIVATE CIRCTRTGDialect + CIRCTSupport MLIRArithDialect MLIRIR MLIRPass diff --git a/lib/Dialect/RTG/Transforms/EmitRTGISAAssemblyPass.cpp b/lib/Dialect/RTG/Transforms/EmitRTGISAAssemblyPass.cpp new file mode 100644 index 000000000000..93be120d5600 --- /dev/null +++ b/lib/Dialect/RTG/Transforms/EmitRTGISAAssemblyPass.cpp @@ -0,0 +1,249 @@ +//===- EmitRTGISAAssemblyPass.cpp - RTG Assembly Emitter ------------------===// +// +// 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 is the main ISA Assembly emitter implementation for the RTG dialect. +// +//===----------------------------------------------------------------------===// + +#include "circt/Dialect/RTG/IR/RTGISAAssemblyOpInterfaces.h" +#include "circt/Dialect/RTG/IR/RTGOps.h" +#include "circt/Dialect/RTG/Transforms/RTGPasses.h" +#include "circt/Support/Path.h" +#include "mlir/IR/Threading.h" +#include "mlir/Support/FileUtilities.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/ToolOutputFile.h" +#include "llvm/Support/raw_ostream.h" +#include + +namespace circt { +namespace rtg { +#define GEN_PASS_DEF_EMITRTGISAASSEMBLYPASS +#include "circt/Dialect/RTG/Transforms/RTGPasses.h.inc" +} // namespace rtg +} // namespace circt + +using namespace circt; +using namespace rtg; + +#define DEBUG_TYPE "emit-rtg-isa-assembly" + +namespace { + +class Emitter { +public: + Emitter(llvm::raw_ostream &os, + const llvm::StringSet &unsupportedInstr) + : os(os), unsupportedInstr(unsupportedInstr) {} + + LogicalResult emitInstruction(InstructionOpInterface instr) { + os << llvm::indent(4); + bool useBinary = unsupportedInstr.contains(instr->getName().getStringRef()); + + // TODO: we cannot just assume that double-slash is the way to do a line + // comment + if (useBinary) + os << "// "; + + SmallVector operands; + for (auto operand : instr->getOperands()) { + auto attr = state.lookup(operand); + if (!attr) + return failure(); + + operands.push_back(attr); + } + + instr.printInstructionAssembly(os, operands); + os << "\n"; + + if (!useBinary) + return success(); + + os << llvm::indent(4); + // TODO: don't hardcode '.word' + os << ".word 0x"; + instr.printInstructionBinary(os, operands); + os << "\n"; + + return success(); + } + + LogicalResult emitTest(rtg::TestOp test, bool emitHeaderFooter = false) { + if (emitHeaderFooter) + os << "// Begin of " << test.getSymName() << "\n\n"; + + for (auto &op : *test.getBody()) { + if (op.hasTrait()) { + SmallVector results; + if (failed(op.fold(results))) + return failure(); + + for (auto [val, res] : llvm::zip(op.getResults(), results)) { + auto attr = res.dyn_cast(); + if (!attr) + return failure(); + + state[val] = attr; + } + + continue; + } + + if (auto instr = dyn_cast(&op)) { + if (failed(emitInstruction(instr))) + return failure(); + + continue; + } + + return op.emitError("emitter unknown RTG operation"); + } + + state.clear(); + + if (emitHeaderFooter) + os << "\n// End of " << test.getSymName() << "\n\n"; + + return success(); + } + +private: + /// Output Stream. + llvm::raw_ostream &os; + + /// Instructions to emit in binary. + const llvm::StringSet &unsupportedInstr; + + /// Evaluated values. + DenseMap state; +}; + +} // namespace + +static void parseUnsupportedInstructionsFile( + const std::string &unsupportedInstructionsFile, + llvm::StringSet &unsupportedInstrs) { + if (!unsupportedInstructionsFile.empty()) { + std::ifstream input(unsupportedInstructionsFile); + std::string token; + while (std::getline(input, token, ',')) { + auto trimmed = StringRef(token).trim(); + if (!trimmed.empty()) + unsupportedInstrs.insert(trimmed); + } + } +} + +static std::unique_ptr +createOutputFile(StringRef filename, StringRef dirname, + function_ref emitError) { + // Determine the output path from the output directory and filename. + SmallString<128> outputFilename(dirname); + appendPossiblyAbsolutePath(outputFilename, filename); + auto outputDir = llvm::sys::path::parent_path(outputFilename); + + // Create the output directory if needed. + std::error_code error = llvm::sys::fs::create_directories(outputDir); + if (error) { + emitError() << "cannot create output directory \"" << outputDir + << "\": " << error.message(); + return {}; + } + + // Open the output file. + std::string errorMessage; + auto output = mlir::openOutputFile(outputFilename, &errorMessage); + if (!output) + emitError() << errorMessage; + return output; +} + +//===----------------------------------------------------------------------===// +// EmitRTGISAAssemblyPass +//===----------------------------------------------------------------------===// + +namespace { +struct EmitRTGISAAssemblyPass + : public rtg::impl::EmitRTGISAAssemblyPassBase { + using Base::Base; + + void runOnOperation() override; + /// Emit each 'rtg.test' into a separate file using the test's name as the + /// filename. + LogicalResult + emitSplit(const llvm::StringSet &unsupportedInstr); + /// Emit all tests into a single file (or print them to stderr if no file path + /// is given). + LogicalResult + emit(const llvm::StringSet &unsupportedInstr); +}; +} // namespace + +void EmitRTGISAAssemblyPass::runOnOperation() { + if ((!path.hasValue() || path.empty()) && splitOutput) { + getOperation().emitError("'split-output' option only valid in combination " + "with a valid 'path' argument"); + return signalPassFailure(); + } + + llvm::StringSet unsupportedInstr( + unsupportedInstructions); + parseUnsupportedInstructionsFile(unsupportedInstructionsFile.getValue(), + unsupportedInstr); + + if (splitOutput) { + if (failed(emitSplit(unsupportedInstr))) + return signalPassFailure(); + + return; + } + + if (failed(emit(unsupportedInstr))) + return signalPassFailure(); +} + +LogicalResult EmitRTGISAAssemblyPass::emit( + const llvm::StringSet &unsupportedInstr) { + std::unique_ptr file; + bool emitToFile = path.hasValue() && !path.empty(); + if (emitToFile) { + file = createOutputFile(path, std::string(), + [&]() { return getOperation().emitError(); }); + if (!file) + return failure(); + + file->keep(); + } + + Emitter emitter(emitToFile ? file->os() : llvm::errs(), unsupportedInstr); + for (auto test : getOperation().getOps()) + if (failed(emitter.emitTest(test, true))) + return failure(); + + return success(); +} + +LogicalResult EmitRTGISAAssemblyPass::emitSplit( + const llvm::StringSet &unsupportedInstr) { + auto tests = getOperation().getOps(); + return failableParallelForEach( + &getContext(), tests.begin(), tests.end(), [&](rtg::TestOp test) { + auto res = createOutputFile(test.getSymName().str() + ".s", path, + [&]() { return test.emitError(); }); + if (!res) + return failure(); + + res->keep(); + return Emitter(res->os(), unsupportedInstr).emitTest(test); + }); +} diff --git a/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split-invalid.mlir b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split-invalid.mlir new file mode 100644 index 000000000000..688c79135f8c --- /dev/null +++ b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split-invalid.mlir @@ -0,0 +1,6 @@ +// RUN: circt-opt --rtg-emit-isa-assembly=split-output=true --verify-diagnostics %s + +// expected-error @below {{'split-output' option only valid in combination with a valid 'path' argument}} +module { + rtg.test @test0 : !rtg.dict<> {} +} diff --git a/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split.mlir b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split.mlir new file mode 100644 index 000000000000..e78879b56388 --- /dev/null +++ b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly-split.mlir @@ -0,0 +1,24 @@ +// RUN: circt-opt --rtg-emit-isa-assembly="path=%T split-output=true" %s && FileCheck %s --input-file=%T/test0.s --check-prefix=CHECK-TEST0 && FileCheck %s --input-file=%T/test1.s --check-prefix=CHECK-TEST1 +// RUN: circt-opt --rtg-emit-isa-assembly="path=%t split-output=false" %s && FileCheck %s --input-file=%t --check-prefixes=CHECK,CHECK-TEST0,CHECK-TEST1 + +// CHECK: Begin of test0 +// CHECK-EMPTY: + +rtg.test @test0 : !rtg.dict<> { + // CHECK-TEST0: ebreak + rtgtest.ebreak +} + +// CHECK-EMPTY: +// CHECK: End of test0 +// CHECK-EMPTY: +// CHECK-NEXT: Begin of test1 +// CHECK-EMPTY: + +rtg.test @test1 : !rtg.dict<> { + // CHECK-TEST1: ecall + rtgtest.ecall +} + +// CHECK-EMPTY: +// CHECK-NEXT: End of test1 diff --git a/test/Dialect/RTG/Transform/emit-rtg-isa-assembly.mlir b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly.mlir new file mode 100644 index 000000000000..951346e52aa9 --- /dev/null +++ b/test/Dialect/RTG/Transform/emit-rtg-isa-assembly.mlir @@ -0,0 +1,58 @@ +// RUN: circt-opt --rtg-emit-isa-assembly %s 2>&1 >/dev/null | FileCheck %s --check-prefix=CHECK-ALLOWED --match-full-lines --strict-whitespace +// RUN: circt-opt --rtg-emit-isa-assembly="unsupported-instructions=rtgtest.ebreak,rtgtest.ecall unsupported-instructions-file=%S/unsupported-instr.txt" %s 2>&1 >/dev/null | FileCheck %s --match-full-lines --strict-whitespace + +// CHECK:// Begin of test0 +// CHECK-EMPTY: +// CHECK-ALLOWED:// Begin of test0 +// CHECK-ALLOWED-EMPTY: + +rtg.test @test0 : !rtg.dict<> { + %rd = rtg.fixed_reg #rtgtest.ra + %rs = rtg.fixed_reg #rtgtest.s0 + %imm = rtgtest.immediate #rtgtest.imm12<0> + + // CHECK-ALLOWED-NEXT: jalr ra, 0(s0) + // CHECK-NEXT: // jalr ra, 0(s0) + // CHECK-NEXT: .word 0x400E7 + rtgtest.jalr %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: lb ra, 0(s0) + // CHECK-NEXT: // lb ra, 0(s0) + // CHECK-NEXT: .word 0x40083 + rtgtest.lb %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: lh ra, 0(s0) + // CHECK-NEXT: // lh ra, 0(s0) + // CHECK-NEXT: .word 0x41083 + rtgtest.lh %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: lw ra, 0(s0) + // CHECK-NEXT: // lw ra, 0(s0) + // CHECK-NEXT: .word 0x42083 + rtgtest.lw %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: lbu ra, 0(s0) + // CHECK-NEXT: // lbu ra, 0(s0) + // CHECK-NEXT: .word 0x44083 + rtgtest.lbu %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: lhu ra, 0(s0) + // CHECK-NEXT: // lhu ra, 0(s0) + // CHECK-NEXT: .word 0x45083 + rtgtest.lhu %rd, %rs, %imm + + // CHECK-ALLOWED-NEXT: ebreak + // CHECK-NEXT: // ebreak + // CHECK-NEXT: .word 0x100073 + rtgtest.ebreak + + // CHECK-ALLOWED-NEXT: ecall + // CHECK-NEXT: // ecall + // CHECK-NEXT: .word 0x73 + rtgtest.ecall +} + +// CHECK-EMPTY: +// CHECK-NEXT:// End of test0 +// CHECK-ALLOWED-EMPTY: +// CHECK-ALLOWED-NEXT:// End of test0 diff --git a/test/Dialect/RTG/Transform/unsupported-instr.txt b/test/Dialect/RTG/Transform/unsupported-instr.txt new file mode 100644 index 000000000000..e895f8b7b861 --- /dev/null +++ b/test/Dialect/RTG/Transform/unsupported-instr.txt @@ -0,0 +1 @@ +rtgtest.jalr,rtgtest.lb,rtgtest.lh,rtgtest.lw,rtgtest.lbu,rtgtest.lhu