[mlir] Improve EnumProp, making it take an EnumInfo (#132349)

This commit improves the `EnumProp` class, causing it to wrap around an
`EnumInfo` just like` EnumAttr` does. This EnumProp also has logic for
converting to/from an integer attribute and for being read and written
as bitcode.

The following variants of `EnumProp` are provided:
- `EnumPropWithAttrForm` - an EnumProp that can be constructed from (and
will be converted to, if `storeInCustomAttribute` is true) a custom
attribute, like an `EnumAttr`, instead of a plain integer. This is meant
for backwards compatibility with code that uses enum attributes.

`NamedEnumProp` adds a "`mnemonic` `<` $enum `>`" syntax around the
enum, replicating a common pattern seen in MLIR printers and allowing
for reduced ambiguity.

`NamedEnumPropWithAttrForm` combines both of these extensions.

(Sadly, bytecode auto-upgrade is hampered by the lack of the ability to
optionally parse an attribute.)

Depends on #132148
This commit is contained in:
Krzysztof Drewniak 2025-04-13 22:46:57 -05:00 committed by GitHub
parent e038c5401c
commit 5ecc0ef6b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 332 additions and 93 deletions

View file

@ -1756,6 +1756,23 @@ that it has a value within the valid range of the enum. If their
wrapper attribute instead of using a bare signless integer attribute
for storage.
### Enum properties
Enums can be wrapped in properties so that they can be stored inline.
This causes a value of the enum's C++ class to become a member of the operation's
property struct and for the operation's verifier to check that the enum's value
is a valid value for the enum.
The basic wrapper is `EnumProp`, which simply takes an `EnumInfo`.
A less ambiguous syntax, namely putting a mnemonic and `<>`s surrounding
the enum is generated with `NamedEnumProp`, which takes a `*EnumInfo`
and a mnemonic string, which becomes part of the property's syntax.
Both of these `EnumProp` types have a `*EnumPropWithAttrForm`, which allows for
transparently upgrading from `EnumAttr`s and optionally retaining those
attributes in the generic form.
## Debugging Tips
### Run `mlir-tblgen` to see the generated content

View file

@ -485,17 +485,16 @@ def DISubprogramFlags : I32BitEnumAttr<
// IntegerOverflowFlags
//===----------------------------------------------------------------------===//
def IOFnone : I32BitEnumAttrCaseNone<"none">;
def IOFnsw : I32BitEnumAttrCaseBit<"nsw", 0>;
def IOFnuw : I32BitEnumAttrCaseBit<"nuw", 1>;
def IOFnone : I32BitEnumCaseNone<"none">;
def IOFnsw : I32BitEnumCaseBit<"nsw", 0>;
def IOFnuw : I32BitEnumCaseBit<"nuw", 1>;
def IntegerOverflowFlags : I32BitEnumAttr<
def IntegerOverflowFlags : I32BitEnum<
"IntegerOverflowFlags",
"LLVM integer overflow flags",
[IOFnone, IOFnsw, IOFnuw]> {
let separator = ", ";
let cppNamespace = "::mlir::LLVM";
let genSpecializedAttr = 0;
let printBitEnumPrimaryGroups = 1;
}
@ -504,6 +503,11 @@ def LLVM_IntegerOverflowFlagsAttr :
let assemblyFormat = "`<` $value `>`";
}
def LLVM_IntegerOverflowFlagsProp :
NamedEnumPropWithAttrForm<IntegerOverflowFlags, "overflow", LLVM_IntegerOverflowFlagsAttr> {
let defaultValue = enum.cppType # "::" # "none";
}
//===----------------------------------------------------------------------===//
// FastmathFlags
//===----------------------------------------------------------------------===//

View file

@ -60,7 +60,7 @@ class LLVM_IntArithmeticOpWithOverflowFlag<string mnemonic, string instName,
list<Trait> traits = []> :
LLVM_ArithmeticOpBase<AnySignlessInteger, mnemonic, instName,
!listconcat([DeclareOpInterfaceMethods<IntegerOverflowFlagsInterface>], traits)> {
dag iofArg = (ins EnumProp<"IntegerOverflowFlags", "", "IntegerOverflowFlags::none">:$overflowFlags);
dag iofArg = (ins LLVM_IntegerOverflowFlagsProp:$overflowFlags);
let arguments = !con(commonArgs, iofArg);
string mlirBuilder = [{
@ -69,7 +69,7 @@ class LLVM_IntArithmeticOpWithOverflowFlag<string mnemonic, string instName,
$res = op;
}];
let assemblyFormat = [{
$lhs `,` $rhs `` custom<OverflowFlags>($overflowFlags) attr-dict `:` type($res)
$lhs `,` $rhs ($overflowFlags^)? attr-dict `:` type($res)
}];
string llvmBuilder =
"$res = builder.Create" # instName #
@ -563,10 +563,10 @@ class LLVM_CastOpWithOverflowFlag<string mnemonic, string instName, Type type,
Type resultType, list<Trait> traits = []> :
LLVM_Op<mnemonic, !listconcat([Pure], [DeclareOpInterfaceMethods<IntegerOverflowFlagsInterface>], traits)>,
LLVM_Builder<"$res = builder.Create" # instName # "($arg, $_resultType, /*Name=*/\"\", op.hasNoUnsignedWrap(), op.hasNoSignedWrap());"> {
let arguments = (ins type:$arg, EnumProp<"IntegerOverflowFlags", "", "IntegerOverflowFlags::none">:$overflowFlags);
let arguments = (ins type:$arg, LLVM_IntegerOverflowFlagsProp:$overflowFlags);
let results = (outs resultType:$res);
let builders = [LLVM_OneResultOpBuilder];
let assemblyFormat = "$arg `` custom<OverflowFlags>($overflowFlags) attr-dict `:` type($arg) `to` type($res)";
let assemblyFormat = "$arg ($overflowFlags^)? attr-dict `:` type($arg) `to` type($res)";
string llvmInstName = instName;
string mlirBuilder = [{
auto op = $_builder.create<$_qualCppClassName>(

View file

@ -10,6 +10,7 @@
#define ENUMATTR_TD
include "mlir/IR/AttrTypeBase.td"
include "mlir/IR/Properties.td"
//===----------------------------------------------------------------------===//
// Enum attribute kinds
@ -552,6 +553,141 @@ class EnumAttr<Dialect dialect, EnumInfo enumInfo, string name = "",
let assemblyFormat = "$value";
}
// A property wrapping by a C++ enum. This class will automatically create bytecode
// serialization logic for the given enum, as well as arranging for parser and
// printer calls.
class EnumProp<EnumInfo enumInfo> : Property<enumInfo.cppType, enumInfo.summary> {
EnumInfo enum = enumInfo;
let description = enum.description;
let predicate = !if(
!isa<BitEnumBase>(enum),
CPred<"(static_cast<" # enum.underlyingType # ">($_self) & ~" # !cast<BitEnumBase>(enum).validBits # ") == 0">,
Or<!foreach(case, enum.enumerants, CPred<"$_self == " # enum.cppType # "::" # case.symbol>)>);
let convertFromAttribute = [{
auto intAttr = ::mlir::dyn_cast_if_present<::mlir::IntegerAttr>($_attr);
if (!intAttr) {
return $_diag() << "expected IntegerAttr storage for }] #
enum.cppType # [{";
}
$_storage = static_cast<}] # enum.cppType # [{>(intAttr.getValue().getZExtValue());
return ::mlir::success();
}];
let convertToAttribute = [{
return ::mlir::IntegerAttr::get(::mlir::IntegerType::get($_ctxt, }] # enum.bitwidth
# [{), static_cast<}] # enum.underlyingType #[{>($_storage));
}];
let writeToMlirBytecode = [{
$_writer.writeVarInt(static_cast<uint64_t>($_storage));
}];
let readFromMlirBytecode = [{
uint64_t rawValue;
if (::mlir::failed($_reader.readVarInt(rawValue)))
return ::mlir::failure();
if (rawValue > std::numeric_limits<}] # enum.underlyingType # [{>::max())
return ::mlir::failure();
$_storage = static_cast<}] # enum.cppType # [{>(rawValue);
}];
let optionalParser = [{
auto value = ::mlir::FieldParser<std::optional<}] # enum.cppType # [{>>::parse($_parser);
if (::mlir::failed(value))
return ::mlir::failure();
if (!(value->has_value()))
return std::nullopt;
$_storage = std::move(**value);
}];
}
// Enum property that can have been (or, if `storeInCustomAttribute` is true, will also
// be stored as) an attribute, in addition to being stored as an integer attribute.
class EnumPropWithAttrForm<EnumInfo enumInfo, Attr attributeForm>
: EnumProp<enumInfo> {
Attr attrForm = attributeForm;
bit storeInCustomAttribute = 0;
let convertFromAttribute = [{
auto customAttr = ::mlir::dyn_cast_if_present<}]
# attrForm.storageType # [{>($_attr);
if (customAttr) {
$_storage = customAttr.getValue();
return ::mlir::success();
}
auto intAttr = ::mlir::dyn_cast_if_present<::mlir::IntegerAttr>($_attr);
if (!intAttr) {
return $_diag() << "expected }] # attrForm.storageType
# [{ or IntegerAttr storage for }] # enum.cppType # [{";
}
$_storage = static_cast<}] # enum.cppType # [{>(intAttr.getValue().getZExtValue());
return ::mlir::success();
}];
let convertToAttribute = !if(storeInCustomAttribute, [{
return }] # attrForm.storageType # [{::get($_ctxt, $_storage);
}], [{
return ::mlir::IntegerAttr::get(::mlir::IntegerType::get($_ctxt, }] # enumInfo.bitwidth
# [{), static_cast<}] # enum.underlyingType #[{>($_storage));
}]);
}
class _namedEnumPropFields<string cppType, string mnemonic> {
code parser = [{
if ($_parser.parseKeyword("}] # mnemonic # [{")
|| $_parser.parseLess()) {
return ::mlir::failure();
}
auto parseRes = ::mlir::FieldParser<}] # cppType # [{>::parse($_parser);
if (::mlir::failed(parseRes) ||
::mlir::failed($_parser.parseGreater())) {
return ::mlir::failure();
}
$_storage = *parseRes;
}];
code optionalParser = [{
if ($_parser.parseOptionalKeyword("}] # mnemonic # [{")) {
return std::nullopt;
}
if ($_parser.parseLess()) {
return ::mlir::failure();
}
auto parseRes = ::mlir::FieldParser<}] # cppType # [{>::parse($_parser);
if (::mlir::failed(parseRes) ||
::mlir::failed($_parser.parseGreater())) {
return ::mlir::failure();
}
$_storage = *parseRes;
}];
code printer = [{
$_printer << "}] # mnemonic # [{<" << $_storage << ">";
}];
}
// An EnumProp which, when printed, is surrounded by mnemonic<>.
// For example, if the enum can be a, b, or c, and the mnemonic is foo,
// the format of this property will be "foo<a>", "foo<b>", or "foo<c>".
class NamedEnumProp<EnumInfo enumInfo, string name>
: EnumProp<enumInfo> {
string mnemonic = name;
let parser = _namedEnumPropFields<enum.cppType, mnemonic>.parser;
let optionalParser = _namedEnumPropFields<enum.cppType, mnemonic>.optionalParser;
let printer = _namedEnumPropFields<enum.cppType, mnemonic>.printer;
}
// A `NamedEnumProp` with an attribute form as in `EnumPropWithAttrForm`.
class NamedEnumPropWithAttrForm<EnumInfo enumInfo, string name, Attr attributeForm>
: EnumPropWithAttrForm<enumInfo, attributeForm> {
string mnemonic = name;
let parser = _namedEnumPropFields<enum.cppType, mnemonic>.parser;
let optionalParser = _namedEnumPropFields<enum.cppType, mnemonic>.optionalParser;
let printer = _namedEnumPropFields<enumInfo.cppType, mnemonic>.printer;
}
class _symbolToValue<EnumInfo enumInfo, string case> {
defvar cases =
!filter(iter, enumInfo.enumerants, !eq(iter.str, case));

View file

@ -239,25 +239,6 @@ def I64Prop : IntProp<"int64_t">;
def I32Property : IntProp<"int32_t">, Deprecated<"moved to shorter name I32Prop">;
def I64Property : IntProp<"int64_t">, Deprecated<"moved to shorter name I64Prop">;
class EnumProp<string storageTypeParam, string desc = "", string default = ""> :
Property<storageTypeParam, desc> {
// TODO: implement predicate for enum validity.
let writeToMlirBytecode = [{
$_writer.writeVarInt(static_cast<uint64_t>($_storage));
}];
let readFromMlirBytecode = [{
uint64_t val;
if (failed($_reader.readVarInt(val)))
return ::mlir::failure();
$_storage = static_cast<}] # storageTypeParam # [{>(val);
}];
let defaultValue = default;
}
class EnumProperty<string storageTypeParam, string desc = "", string default = ""> :
EnumProp<storageTypeParam, desc, default>,
Deprecated<"moved to shorter name EnumProp">;
// Note: only a class so we can deprecate the old name
class _cls_StringProp : Property<"std::string", "string"> {
let interfaceType = "::llvm::StringRef";

View file

@ -49,71 +49,6 @@ using mlir::LLVM::tailcallkind::getMaxEnumValForTailCallKind;
#include "mlir/Dialect/LLVMIR/LLVMOpsDialect.cpp.inc"
//===----------------------------------------------------------------------===//
// Property Helpers
//===----------------------------------------------------------------------===//
//===----------------------------------------------------------------------===//
// IntegerOverflowFlags
//===----------------------------------------------------------------------===//
namespace mlir {
static Attribute convertToAttribute(MLIRContext *ctx,
IntegerOverflowFlags flags) {
return IntegerOverflowFlagsAttr::get(ctx, flags);
}
static LogicalResult
convertFromAttribute(IntegerOverflowFlags &flags, Attribute attr,
function_ref<InFlightDiagnostic()> emitError) {
auto flagsAttr = dyn_cast<IntegerOverflowFlagsAttr>(attr);
if (!flagsAttr) {
return emitError() << "expected 'overflowFlags' attribute to be an "
"IntegerOverflowFlagsAttr, but got "
<< attr;
}
flags = flagsAttr.getValue();
return success();
}
} // namespace mlir
static ParseResult parseOverflowFlags(AsmParser &p,
IntegerOverflowFlags &flags) {
if (failed(p.parseOptionalKeyword("overflow"))) {
flags = IntegerOverflowFlags::none;
return success();
}
if (p.parseLess())
return failure();
do {
StringRef kw;
SMLoc loc = p.getCurrentLocation();
if (p.parseKeyword(&kw))
return failure();
std::optional<IntegerOverflowFlags> flag =
symbolizeIntegerOverflowFlags(kw);
if (!flag)
return p.emitError(loc,
"invalid overflow flag: expected nsw, nuw, or none");
flags = flags | *flag;
} while (succeeded(p.parseOptionalComma()));
return p.parseGreater();
}
static void printOverflowFlags(AsmPrinter &p, Operation *op,
IntegerOverflowFlags flags) {
if (flags == IntegerOverflowFlags::none)
return;
p << " overflow<";
SmallVector<StringRef, 2> strs;
if (bitEnumContainsAny(flags, IntegerOverflowFlags::nsw))
strs.push_back("nsw");
if (bitEnumContainsAny(flags, IntegerOverflowFlags::nuw))
strs.push_back("nuw");
llvm::interleaveComma(strs, p);
p << ">";
}
//===----------------------------------------------------------------------===//
// Attribute Helpers
//===----------------------------------------------------------------------===//

View file

@ -28,3 +28,78 @@ func.func @test_parse_invalid_attr() -> () {
// expected-error@+1 {{failed to parse TestEnumAttr parameter 'value'}}
test.op_with_enum 1 : index
}
// -----
func.func @test_non_keyword_prop_enum() -> () {
// expected-error@+2 {{expected keyword for a test enum}}
// expected-error@+1 {{invalid value for property value, expected a test enum}}
test.op_with_enum_prop 0
return
}
// -----
func.func @test_wrong_keyword_prop_enum() -> () {
// expected-error@+2 {{expected one of [first, second, third] for a test enum, got: fourth}}
// expected-error@+1 {{invalid value for property value, expected a test enum}}
test.op_with_enum_prop fourth
}
// -----
func.func @test_bad_integer() -> () {
// expected-error@+1 {{op property 'value' failed to satisfy constraint: a test enum}}
"test.op_with_enum_prop"() <{value = 4 : i32}> {} : () -> ()
}
// -----
func.func @test_bit_enum_prop_not_keyword() -> () {
// expected-error@+2 {{expected keyword for a test bit enum}}
// expected-error@+1 {{invalid value for property value1, expected a test bit enum}}
test.op_with_bit_enum_prop 0
return
}
// -----
func.func @test_bit_enum_prop_wrong_keyword() -> () {
// expected-error@+2 {{expected one of [read, write, execute] for a test bit enum, got: chroot}}
// expected-error@+1 {{invalid value for property value1, expected a test bit enum}}
test.op_with_bit_enum_prop read, chroot : ()
return
}
// -----
func.func @test_bit_enum_prop_bad_value() -> () {
// expected-error@+1 {{op property 'value2' failed to satisfy constraint: a test bit enum}}
"test.op_with_bit_enum_prop"() <{value1 = 7 : i32, value2 = 8 : i32}> {} : () -> ()
return
}
// -----
func.func @test_bit_enum_prop_named_wrong_keyword() -> () {
// expected-error@+2 {{expected 'bit_enum'}}
// expected-error@+1 {{invalid value for property value1, expected a test bit enum}}
test.op_with_bit_enum_prop_named foo<read, execute>
return
}
// -----
func.func @test_bit_enum_prop_named_not_open() -> () {
// expected-error@+2 {{expected '<'}}
// expected-error@+1 {{invalid value for property value1, expected a test bit enum}}
test.op_with_bit_enum_prop_named bit_enum read, execute>
}
// -----
func.func @test_bit_enum_prop_named_not_closed() -> () {
// expected-error@+2 {{expected '>'}}
// expected-error@+1 {{invalid value for property value1, expected a test bit enum}}
test.op_with_bit_enum_prop_named bit_enum<read, execute +
}

View file

@ -35,3 +35,48 @@ func.func @test_match_op_with_bit_enum() -> () {
test.op_with_bit_enum <execute, write> tag 0 : i32
return
}
// CHECK-LABEL: @test_enum_prop
func.func @test_enum_prop() -> () {
// CHECK: test.op_with_enum_prop first
test.op_with_enum_prop first
// CHECK: test.op_with_enum_prop first
"test.op_with_enum_prop"() <{value = 0 : i32}> {} : () -> ()
// CHECK: test.op_with_enum_prop_attr_form <{value = 0 : i32}>
test.op_with_enum_prop_attr_form <{value = 0 : i32}>
// CHECK: test.op_with_enum_prop_attr_form <{value = 1 : i32}>
test.op_with_enum_prop_attr_form <{value = #test<enum second>}>
// CHECK: test.op_with_enum_prop_attr_form_always <{value = #test<enum first>}>
test.op_with_enum_prop_attr_form_always <{value = #test<enum first>}>
// CHECK: test.op_with_enum_prop_attr_form_always <{value = #test<enum second>}
test.op_with_enum_prop_attr_form_always <{value = #test<enum second>}>
return
}
// CHECK-LABEL @test_bit_enum_prop()
func.func @test_bit_enum_prop() -> () {
// CHECK: test.op_with_bit_enum_prop read : ()
test.op_with_bit_enum_prop read read : ()
// CHECK: test.op_with_bit_enum_prop read, write write, execute
test.op_with_bit_enum_prop read, write write, execute : ()
// CHECK: test.op_with_bit_enum_prop read, execute write
"test.op_with_bit_enum_prop"() <{value1 = 5 : i32, value2 = 2 : i32}> {} : () -> ()
// CHECK: test.op_with_bit_enum_prop read, write, execute
test.op_with_bit_enum_prop read, write, execute : ()
// CHECK: test.op_with_bit_enum_prop_named bit_enum<read>{{$}}
test.op_with_bit_enum_prop_named bit_enum<read> bit_enum<read>
// CHECK: test.op_with_bit_enum_prop_named bit_enum<read, write> bit_enum<write, execute>
test.op_with_bit_enum_prop_named bit_enum<read, write> bit_enum<write, execute>
// CHECK: test.op_with_bit_enum_prop_named bit_enum<read, write, execute>
test.op_with_bit_enum_prop_named bit_enum<read, write, execute>
return
}

View file

@ -423,6 +423,52 @@ def : Pat<(OpWithEnum ConstantEnumCase<TestEnumAttr, "first">:$value,
(OpWithEnum ConstantEnumCase<TestEnumAttr, "second">,
ConstantAttr<I32Attr, "1">)>;
//===----------------------------------------------------------------------===//
// Test Enum Properties
//===----------------------------------------------------------------------===//
// Define the enum property.
def TestEnumProp : EnumProp<TestEnum>;
// Define an op that contains the enum property.
def OpWithEnumProp : TEST_Op<"op_with_enum_prop"> {
let arguments = (ins TestEnumProp:$value);
let assemblyFormat = "$value attr-dict";
}
def TestEnumPropAttrForm : EnumPropWithAttrForm<TestEnum, TestEnumAttr>;
def OpWithEnumPropAttrForm : TEST_Op<"op_with_enum_prop_attr_form"> {
let arguments = (ins TestEnumPropAttrForm:$value);
let assemblyFormat = "prop-dict attr-dict";
}
def TestEnumPropAttrFormAlways : EnumPropWithAttrForm<TestEnum, TestEnumAttr> {
let storeInCustomAttribute = 1;
}
def OpWithEnumPropAttrFormAlways : TEST_Op<"op_with_enum_prop_attr_form_always"> {
let arguments = (ins TestEnumPropAttrFormAlways:$value);
let assemblyFormat = "prop-dict attr-dict";
}
def TestBitEnumProp : EnumProp<TestBitEnum> {
let defaultValue = TestBitEnum.cppType # "::Read";
}
def OpWithTestBitEnum : TEST_Op<"op_with_bit_enum_prop"> {
let arguments = (ins
TestBitEnumProp:$value1,
TestBitEnumProp:$value2);
let assemblyFormat = "$value1 ($value2^)? attr-dict `:` `(``)`";
}
def TestBitEnumPropNamed : NamedEnumProp<TestBitEnum, "bit_enum"> {
let defaultValue = TestBitEnum.cppType # "::Read";
}
def OpWithBitEnumPropNamed : TEST_Op<"op_with_bit_enum_prop_named"> {
let arguments = (ins
TestBitEnumPropNamed:$value1,
TestBitEnumPropNamed:$value2);
let assemblyFormat = "$value1 ($value2^)? attr-dict";
}
//===----------------------------------------------------------------------===//
// Test Bit Enum Attributes
//===----------------------------------------------------------------------===//