From 93d0536ece91b6bfa878d9ff05d1a5eb13acb65a Mon Sep 17 00:00:00 2001 From: Martin Erhart Date: Mon, 2 Dec 2024 09:47:44 +0000 Subject: [PATCH] [RTG] Add TestOp, TargetOp, and DictType (#7856) --- include/circt/Dialect/RTG/IR/RTGInterfaces.td | 7 ++ include/circt/Dialect/RTG/IR/RTGOps.td | 80 +++++++++++++++++++ include/circt/Dialect/RTG/IR/RTGTypes.h | 24 ++++++ include/circt/Dialect/RTG/IR/RTGTypes.td | 25 ++++++ include/circt/Dialect/RTG/IR/RTGVisitors.h | 19 +++-- .../circt/Dialect/RTGTest/IR/RTGTestOps.td | 2 +- lib/Dialect/RTG/IR/RTGOps.cpp | 23 ++++++ lib/Dialect/RTG/IR/RTGTypes.cpp | 70 ++++++++++++++++ test/Dialect/RTG/IR/basic.mlir | 24 ++++++ test/Dialect/RTG/IR/errors.mlir | 34 ++++++++ test/Dialect/RTGTest/IR/basic.mlir | 8 +- 11 files changed, 303 insertions(+), 13 deletions(-) diff --git a/include/circt/Dialect/RTG/IR/RTGInterfaces.td b/include/circt/Dialect/RTG/IR/RTGInterfaces.td index 8d59f14e2f63..a40ebe41e9a2 100644 --- a/include/circt/Dialect/RTG/IR/RTGInterfaces.td +++ b/include/circt/Dialect/RTG/IR/RTGInterfaces.td @@ -10,6 +10,7 @@ #define CIRCT_DIALECT_RTG_IR_RTGINTERFACES_TD include "mlir/IR/Interfaces.td" +include "mlir/IR/OpBase.td" def ContextResourceOpInterface : OpInterface<"ContextResourceOpInterface"> { let description = [{ @@ -30,6 +31,12 @@ def ContextResourceOpInterface : OpInterface<"ContextResourceOpInterface"> { ]; } +/// Context resources can only be defined inside the `rtg.target` operation. +def ContextResourceDefining : TraitList<[ + DeclareOpInterfaceMethods, + HasParent<"::circt::rtg::TargetOp">, +]>; + def ContextResourceTypeInterface : TypeInterface< "ContextResourceTypeInterface"> { let description = [{ diff --git a/include/circt/Dialect/RTG/IR/RTGOps.td b/include/circt/Dialect/RTG/IR/RTGOps.td index 4c79e50786e3..95985be015cb 100644 --- a/include/circt/Dialect/RTG/IR/RTGOps.td +++ b/include/circt/Dialect/RTG/IR/RTGOps.td @@ -140,3 +140,83 @@ def SetDifferenceOp : RTGOp<"set_difference", [ $original `,` $diff `:` qualified(type($output)) attr-dict }]; } + +//===- Test Specification Operations --------------------------------------===// + +def TestOp : RTGOp<"test", [ + IsolatedFromAbove, + Symbol, + SingleBlock, + NoTerminator, + HasParent<"mlir::ModuleOp"> +]> { + let summary = "the root of a test"; + let description = [{ + This operation declares the root of a randomized or directed test. + The target attribute specifies requirements of this test. These can be + refined by `rtg.require` operations inside this operation's body. A test + can only be matched with a target if the target fulfills all the test's + requirements. However, the target may provide more than the test requires. + For example, if the target allows execution in a user and privileged mode, + but the test only requires and runs in user mode, it can still be matched + with that target. + + By default each test can be matched with all targets that fulfill its + requirements, but the user should be able to specify more constraints on the + matching procedure. + + The body of this operation shall be processed the same way as an + `rtg.sequence`'s body with the exception of the block arguments. + The arguments must match the fields of the dict type in the target attribute + exactly. The test must not have any additional arguments and cannot be + referenced by an `rtg.sequence_closure` operation. + }]; + + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$target); + let regions = (region SizedRegion<1>:$bodyRegion); + + let assemblyFormat = [{ + $sym_name `:` $target attr-dict-with-keyword $bodyRegion + }]; + + let hasRegionVerifier = 1; +} + +def TargetOp : RTGOp<"target", [ + IsolatedFromAbove, + Symbol, + NoRegionArguments, + SingleBlockImplicitTerminator<"rtg::YieldOp">, + HasParent<"mlir::ModuleOp"> +]> { + let summary = "defines a test target"; + let description = [{ + This operation specifies capabilities of a specific test target and can + provide additional information about it. These are added as operands to the + `yield` terminator and implicitly packed up into an `!rtg.dict` type which + is passed to tests that are matched with this target. + + These capabilities can, for example, consist of the number of CPUs, supported + priviledge modes, available memories, etc. + }]; + + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$target); + let regions = (region SizedRegion<1>:$bodyRegion); + + let assemblyFormat = [{ + $sym_name `:` $target attr-dict-with-keyword $bodyRegion + }]; + + let hasRegionVerifier = 1; +} + +def YieldOp : RTGOp<"yield", [Pure, Terminator]> { + let summary = "terminates RTG operation regions"; + + let arguments = (ins Variadic:$operands); + let assemblyFormat = "($operands^ `:` type($operands))? attr-dict"; + + let builders = [OpBuilder<(ins), [{ /* nothing to do */ }]>]; +} diff --git a/include/circt/Dialect/RTG/IR/RTGTypes.h b/include/circt/Dialect/RTG/IR/RTGTypes.h index 084e202ae7d3..23fc37cd3fa0 100644 --- a/include/circt/Dialect/RTG/IR/RTGTypes.h +++ b/include/circt/Dialect/RTG/IR/RTGTypes.h @@ -13,6 +13,30 @@ #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/Types.h" +namespace circt { +namespace rtg { + +/// Defines an entry in an `!rtg.dict`. +struct DictEntry { + mlir::StringAttr name; + mlir::Type type; +}; + +inline bool operator<(const DictEntry &entry, const DictEntry &other) { + return entry.name.getValue() < other.name.getValue(); +} + +inline bool operator==(const DictEntry &entry, const DictEntry &other) { + return entry.name == other.name && entry.type == other.type; +} + +inline llvm::hash_code hash_value(const DictEntry &entry) { + return llvm::hash_combine(entry.name, entry.type); +} + +} // namespace rtg +} // namespace circt + #define GET_TYPEDEF_CLASSES #include "circt/Dialect/RTG/IR/RTGTypes.h.inc" diff --git a/include/circt/Dialect/RTG/IR/RTGTypes.td b/include/circt/Dialect/RTG/IR/RTGTypes.td index a76c1dd1fcf8..7778c65f2448 100644 --- a/include/circt/Dialect/RTG/IR/RTGTypes.td +++ b/include/circt/Dialect/RTG/IR/RTGTypes.td @@ -47,4 +47,29 @@ class SetTypeOf : ContainerType< elementType, SetType.predicate, "llvm::cast($_self).getElementType()", "set">; +def DictType : RTGTypeDef<"Dict"> { + let summary = "a dictionary"; + let description = [{ + This type is a dictionary with a static set of entries. This datatype does + not make any assumptions about how the values are stored (could be a struct, + a map, etc.). Furthermore, two values of this type should be considered + equivalent if they have the same set of entry names and types and the values + match for each entry, independent of the order. + }]; + + let parameters = (ins + ArrayRefParameter<"::circt::rtg::DictEntry", "dict entries">:$entries); + + let extraClassDeclaration = [{ + /// Checks if the types of the dictionary entries match the ones in the + /// given type range. The dictionary entries are sorted by ascending names. + bool entryTypesMatch(mlir::TypeRange types) const; + }]; + + let mnemonic = "dict"; + + let hasCustomAssemblyFormat = 1; + let genVerifyDecl = 1; +} + #endif // CIRCT_DIALECT_RTG_IR_RTGTYPES_TD diff --git a/include/circt/Dialect/RTG/IR/RTGVisitors.h b/include/circt/Dialect/RTG/IR/RTGVisitors.h index 580f3ab55c00..de11a8ee65e3 100644 --- a/include/circt/Dialect/RTG/IR/RTGVisitors.h +++ b/include/circt/Dialect/RTG/IR/RTGVisitors.h @@ -31,10 +31,10 @@ class RTGOpVisitor { auto *thisCast = static_cast(this); return TypeSwitch(op) .template Case( - [&](auto expr) -> ResultType { - return thisCast->visitOp(expr, args...); - }) + SetSelectRandomOp, SetDifferenceOp, InvokeSequenceOp, + TestOp, TargetOp, YieldOp>([&](auto expr) -> ResultType { + return thisCast->visitOp(expr, args...); + }) .template Case( [&](auto expr) -> ResultType { return thisCast->visitContextResourceOp(expr, args...); @@ -79,6 +79,9 @@ class RTGOpVisitor { HANDLE(SetCreateOp, Unhandled); HANDLE(SetSelectRandomOp, Unhandled); HANDLE(SetDifferenceOp, Unhandled); + HANDLE(TestOp, Unhandled); + HANDLE(TargetOp, Unhandled); + HANDLE(YieldOp, Unhandled); #undef HANDLE }; @@ -90,9 +93,10 @@ class RTGTypeVisitor { ResultType dispatchTypeVisitor(Type type, ExtraArgs... args) { auto *thisCast = static_cast(this); return TypeSwitch(type) - .template Case([&](auto expr) -> ResultType { - return thisCast->visitType(expr, args...); - }) + .template Case( + [&](auto expr) -> ResultType { + return thisCast->visitType(expr, args...); + }) .template Case( [&](auto expr) -> ResultType { return thisCast->visitContextResourceType(expr, args...); @@ -134,6 +138,7 @@ class RTGTypeVisitor { HANDLE(SequenceType, Unhandled); HANDLE(SetType, Unhandled); + HANDLE(DictType, Unhandled); #undef HANDLE }; diff --git a/include/circt/Dialect/RTGTest/IR/RTGTestOps.td b/include/circt/Dialect/RTGTest/IR/RTGTestOps.td index 401f3d79beae..e64f6c8ac21e 100644 --- a/include/circt/Dialect/RTGTest/IR/RTGTestOps.td +++ b/include/circt/Dialect/RTGTest/IR/RTGTestOps.td @@ -21,7 +21,7 @@ class RTGTestOp traits = []> : def CPUDeclOp : RTGTestOp<"cpu_decl", [ Pure, ConstantLike, - DeclareOpInterfaceMethods, + ContextResourceDefining, ]> { let summary = "declare a CPU"; let description = [{ diff --git a/lib/Dialect/RTG/IR/RTGOps.cpp b/lib/Dialect/RTG/IR/RTGOps.cpp index 6aebf52844f9..de89cd77ae77 100644 --- a/lib/Dialect/RTG/IR/RTGOps.cpp +++ b/lib/Dialect/RTG/IR/RTGOps.cpp @@ -78,6 +78,29 @@ LogicalResult SetCreateOp::verify() { return success(); } +//===----------------------------------------------------------------------===// +// TestOp +//===----------------------------------------------------------------------===// + +LogicalResult TestOp::verifyRegions() { + if (!getTarget().entryTypesMatch(getBody()->getArgumentTypes())) + return emitOpError("argument types must match dict entry types"); + + return success(); +} + +//===----------------------------------------------------------------------===// +// TargetOp +//===----------------------------------------------------------------------===// + +LogicalResult TargetOp::verifyRegions() { + if (!getTarget().entryTypesMatch( + getBody()->getTerminator()->getOperandTypes())) + return emitOpError("terminator operand types must match dict entry types"); + + return success(); +} + //===----------------------------------------------------------------------===// // TableGen generated logic. //===----------------------------------------------------------------------===// diff --git a/lib/Dialect/RTG/IR/RTGTypes.cpp b/lib/Dialect/RTG/IR/RTGTypes.cpp index 676f188a49fd..b05791aec019 100644 --- a/lib/Dialect/RTG/IR/RTGTypes.cpp +++ b/lib/Dialect/RTG/IR/RTGTypes.cpp @@ -19,6 +19,76 @@ using namespace rtg; #define GET_TYPEDEF_CLASSES #include "circt/Dialect/RTG/IR/RTGTypes.cpp.inc" +//===----------------------------------------------------------------------===// +// DictType +//===----------------------------------------------------------------------===/ + +LogicalResult DictType::verify(function_ref emitError, + ArrayRef entries) { + StringAttr last; + for (auto entry : entries) { + if (entry.name.empty()) + return emitError() << "empty strings not allowed as entry names"; + + if (last && entry.name.getValue() <= last.getValue()) + return emitError() << "dictionary must be sorted by names and contain no " + "duplicates, first violation at entry '" + << entry.name.getValue() << "'"; + + last = entry.name; + } + + return success(); +} + +Type DictType::parse(AsmParser &p) { + SmallVector entries; + auto loc = p.getCurrentLocation(); + + auto parseResult = p.parseCommaSeparatedList( + mlir::AsmParser::Delimiter::LessGreater, [&]() -> ParseResult { + std::string name; + Type type; + loc = p.getCurrentLocation(); + + if (p.parseKeywordOrString(&name) || p.parseColon() || + p.parseType(type)) + return failure(); + + DictEntry entry; + entry.name = StringAttr::get(p.getContext(), name); + entry.type = type; + entries.emplace_back(entry); + return success(); + }); + + if (failed(parseResult)) + return Type(); + + auto emitError = [&]() { return p.emitError(loc); }; + + // Call 'getChecked' here such that we do not have to repeat the verification + // checks in the parser here, but still get the errors reported at meaningful + // locations. + return getChecked(emitError, p.getContext(), entries); +} + +void DictType::print(AsmPrinter &p) const { + p << '<'; + llvm::interleaveComma(getEntries(), p, [&](auto entry) { + p.printKeywordOrString(entry.name.getValue()); + p << ": " << entry.type; + }); + p << ">"; +} + +bool DictType::entryTypesMatch(TypeRange types) const { + return llvm::equal(getEntries(), types, + [](const DictEntry &entry, const Type &type) { + return entry.type == type; + }); +} + void circt::rtg::RTGDialect::registerTypes() { addTypes< #define GET_TYPEDEF_LIST diff --git a/test/Dialect/RTG/IR/basic.mlir b/test/Dialect/RTG/IR/basic.mlir index eae6e394a66f..f0070e6726d7 100644 --- a/test/Dialect/RTG/IR/basic.mlir +++ b/test/Dialect/RTG/IR/basic.mlir @@ -38,3 +38,27 @@ func.func @sets(%arg0: i32, %arg1: i32) { return } + +// CHECK-LABEL: rtg.target @empty_target : !rtg.dict<> { +// CHECK-NOT: rtg.yield +rtg.target @empty_target : !rtg.dict<> { + rtg.yield +} + +// CHECK-LABEL: rtg.test @empty_test : !rtg.dict<> { +rtg.test @empty_test : !rtg.dict<> { } + +// CHECK-LABEL: rtg.target @target : !rtg.dict { +// CHECK: rtg.yield %{{.*}}, %{{.*}} : i32, i32 +// CHECK: } +rtg.target @target : !rtg.dict { + %1 = arith.constant 4 : i32 + rtg.yield %1, %1 : i32, i32 +} + +// CHECK-LABEL: rtg.test @test : !rtg.dict { +// CHECK: ^bb0(%arg0: i32, %arg1: i32): +// CHECK: } +rtg.test @test : !rtg.dict { +^bb0(%arg0: i32, %arg1: i32): +} diff --git a/test/Dialect/RTG/IR/errors.mlir b/test/Dialect/RTG/IR/errors.mlir index 66a4c818cff9..fb0f334624f0 100644 --- a/test/Dialect/RTG/IR/errors.mlir +++ b/test/Dialect/RTG/IR/errors.mlir @@ -16,3 +16,37 @@ rtg.sequence @seq0 { // expected-error @below {{referenced 'rtg.sequence' op's argument types must match 'args' types}} rtg.sequence_closure @seq0 + +// ----- + +// expected-error @below {{terminator operand types must match dict entry types}} +rtg.target @target : !rtg.dict { + rtg.yield +} + +// ----- + +// expected-error @below {{argument types must match dict entry types}} +rtg.test @test : !rtg.dict { +} + +// ----- + +// expected-error @below {{dictionary must be sorted by names and contain no duplicates, first violation at entry 'a'}} +rtg.test @test : !rtg.dict { +^bb0(%arg0: i32, %arg1: i32): +} + +// ----- + +// expected-error @below {{dictionary must be sorted by names and contain no duplicates, first violation at entry 'a'}} +rtg.test @test : !rtg.dict { +^bb0(%arg0: i32, %arg1: i32): +} + +// ----- + +// expected-error @below {{empty strings not allowed as entry names}} +rtg.test @test : !rtg.dict<"": i32> { +^bb0(%arg0: i32): +} diff --git a/test/Dialect/RTGTest/IR/basic.mlir b/test/Dialect/RTGTest/IR/basic.mlir index 897bd52dc669..d46aeac3e08c 100644 --- a/test/Dialect/RTGTest/IR/basic.mlir +++ b/test/Dialect/RTGTest/IR/basic.mlir @@ -1,11 +1,9 @@ // RUN: circt-opt %s | FileCheck %s -// TODO: replace this with `rtg.target` because ops implementing -// ContextResourceOpInterface are only allowed in such target operations. // CHECK-LABEL: @cpus -rtg.sequence @cpus { -// CHECK: !rtgtest.cpu -^bb0(%arg0: !rtgtest.cpu): +// CHECK-SAME: !rtgtest.cpu +rtg.target @cpus : !rtg.dict { // CHECK: %0 = rtgtest.cpu_decl 0 %0 = rtgtest.cpu_decl 0 + rtg.yield %0 : !rtgtest.cpu }