[Mlir-commits] [llvm] [mlir] [RFC][TableGen] Add let append/prepend syntax for field concatenation (PR #182382)

Henrich Lauko llvmlistbot at llvm.org
Sat Feb 21 13:05:58 PST 2026


https://github.com/xlauko updated https://github.com/llvm/llvm-project/pull/182382

>From 27ddcad69886a069243365b4632b8793757e2fd1 Mon Sep 17 00:00:00 2001
From: xlauko <xlauko at mail.muni.cz>
Date: Thu, 19 Feb 2026 22:52:42 +0100
Subject: [PATCH] [TableGen] Add let append/prepend syntax for field
 concatenation

---
 llvm/docs/TableGen/ProgRef.rst                |  47 +++-
 llvm/lib/TableGen/TGParser.cpp                |  84 ++++++-
 llvm/lib/TableGen/TGParser.h                  |  14 +-
 .../TableGen/let-append-bitrange-error.td     |  13 ++
 llvm/test/TableGen/let-append-error.td        |  13 ++
 llvm/test/TableGen/let-append.td              | 219 ++++++++++++++++++
 llvm/test/TableGen/let-prepend-error.td       |  13 ++
 .../DefiningDialects/AttributesAndTypes.md    |  24 ++
 mlir/docs/DefiningDialects/Operations.md      |  34 +++
 mlir/test/mlir-tblgen/attrdefs.td             |  82 +++++++
 mlir/test/mlir-tblgen/op-decl-and-defs.td     |  98 ++++++++
 mlir/test/mlir-tblgen/typedefs.td             |  82 +++++++
 12 files changed, 710 insertions(+), 13 deletions(-)
 create mode 100644 llvm/test/TableGen/let-append-bitrange-error.td
 create mode 100644 llvm/test/TableGen/let-append-error.td
 create mode 100644 llvm/test/TableGen/let-append.td
 create mode 100644 llvm/test/TableGen/let-prepend-error.td

diff --git a/llvm/docs/TableGen/ProgRef.rst b/llvm/docs/TableGen/ProgRef.rst
index 2e66778b42ae1..e03b4ce830267 100644
--- a/llvm/docs/TableGen/ProgRef.rst
+++ b/llvm/docs/TableGen/ProgRef.rst
@@ -686,9 +686,14 @@ arguments.
 .. productionlist::
    Body: ";" | "{" `BodyItem`* "}"
    BodyItem: `Type` `TokIdentifier` ["=" `Value`] ";"
-           :| "let" `TokIdentifier` ["{" `RangeList` "}"] "=" `Value` ";"
+           :| "let" [`LetMode`] `TokIdentifier` ["{" `RangeList` "}"] "=" `Value` ";"
            :| "defvar" `TokIdentifier` "=" `Value` ";"
            :| `Assert`
+   LetMode: "append" | "prepend"
+
+Note that ``append`` and ``prepend`` are context-sensitive keywords: they are
+only recognized as modifiers immediately after ``let``. In all other positions,
+they remain valid identifiers (e.g., usable as field names).
 
 A field definition in the body specifies a field to be included in the class
 or record. If no initial value is specified, then the field's value is
@@ -700,6 +705,34 @@ for fields defined directly in the body or fields inherited from parent
 classes.  A :token:`RangeList` can be specified to reset certain bits in a
 ``bit<n>`` field.
 
+The ``let append`` and ``let prepend`` forms concatenate a value with the
+field's current value instead of replacing it. For ``append``, the new value
+is added after the current value; for ``prepend``, it is added before. The
+supported types and concatenation operators are:
+
+* ``list<T>``: uses ``!listconcat``
+* ``string`` / ``code``: uses ``!strconcat``
+* ``dag``: uses ``!con``
+
+If the field is currently unset (``?``), ``let append`` and ``let prepend``
+simply set the value directly. This is useful for accumulating values across
+a class hierarchy:
+
+.. code-block:: text
+
+  class Base {
+    list<int> items = [2, 3];
+  }
+  class Middle : Base {
+    let append items = [4];       // items = [2, 3, 4]
+  }
+  def Concrete : Middle {
+    let prepend items = [1];      // items = [1, 2, 3, 4]
+  }
+
+A plain ``let`` (without ``append``/``prepend``) always replaces the current
+value, which can be used to opt out of accumulated values.
+
 The ``defvar`` form defines a variable whose value can be used in other
 value expressions within the body. The variable is not a field: it does not
 become a field of the class or record being defined. Variables are provided
@@ -890,7 +923,7 @@ statements within the scope of the ``let``.
    Let:  "let" `LetList` "in" "{" `Statement`* "}"
       :| "let" `LetList` "in" `Statement`
    LetList: `LetItem` ("," `LetItem`)*
-   LetItem: `TokIdentifier` ["<" `RangeList` ">"] "=" `Value`
+   LetItem: [`LetMode`] `TokIdentifier` ["<" `RangeList` ">"] "=" `Value`
 
 The ``let`` statement establishes a scope, which is a sequence of statements
 in braces or a single statement with no braces. The bindings in the
@@ -927,6 +960,16 @@ statements can be nested.
 Note that a top-level ``let`` will not override fields defined in the classes or records
 themselves.
 
+Top-level ``let`` also supports ``append`` and ``prepend`` modes, which
+concatenate the value with the field's current value instead of replacing it.
+See the `Body`_ section for the supported types and semantics.
+
+.. code-block:: text
+
+  let append traits = [NewTrait] in {
+    def MyRecord : Base;
+  }
+
 
 ``multiclass`` --- define multiple records
 ------------------------------------------
diff --git a/llvm/lib/TableGen/TGParser.cpp b/llvm/lib/TableGen/TGParser.cpp
index 3d31d8e2717b1..0a533625cce72 100644
--- a/llvm/lib/TableGen/TGParser.cpp
+++ b/llvm/lib/TableGen/TGParser.cpp
@@ -238,7 +238,8 @@ bool TGParser::AddValue(Record *CurRec, SMLoc Loc, const RecordVal &RV) {
 /// Return true on error, false on success.
 bool TGParser::SetValue(Record *CurRec, SMLoc Loc, const Init *ValName,
                         ArrayRef<unsigned> BitList, const Init *V,
-                        bool AllowSelfAssignment, bool OverrideDefLoc) {
+                        bool AllowSelfAssignment, bool OverrideDefLoc,
+                        LetMode Mode) {
   if (!V)
     return false;
 
@@ -250,6 +251,41 @@ bool TGParser::SetValue(Record *CurRec, SMLoc Loc, const Init *ValName,
     return Error(Loc,
                  "Value '" + ValName->getAsUnquotedString() + "' unknown!");
 
+  // Handle append/prepend by concatenating with the current value.
+  if (Mode != LetMode::Replace) {
+    assert(Mode == LetMode::Append || Mode == LetMode::Prepend);
+
+    if (!BitList.empty())
+      return Error(Loc, "cannot use append/prepend with bit range");
+
+    const Init *CurrentValue = RV->getValue();
+    const RecTy *FieldType = RV->getType();
+
+    // If the current value is unset, just assign the new value directly.
+    if (!isa<UnsetInit>(CurrentValue)) {
+      const bool IsAppendMode = Mode == LetMode::Append;
+
+      const Init *LHS = IsAppendMode ? CurrentValue : V;
+      const Init *RHS = IsAppendMode ? V : CurrentValue;
+
+      BinOpInit::BinaryOp ConcatOp;
+      if (isa<ListRecTy>(FieldType))
+        ConcatOp = BinOpInit::LISTCONCAT;
+      else if (isa<StringRecTy>(FieldType))
+        ConcatOp = BinOpInit::STRCONCAT;
+      else if (isa<DagRecTy>(FieldType))
+        ConcatOp = BinOpInit::CONCAT;
+      else
+        return Error(Loc, Twine("cannot ") +
+                              (IsAppendMode ? "append to" : "prepend to") +
+                              " field '" + ValName->getAsUnquotedString() +
+                              "' of type '" + FieldType->getAsString() +
+                              "' (expected list, string, code, or dag)");
+
+      V = BinOpInit::get(ConcatOp, LHS, RHS, FieldType)->Fold(CurRec);
+    }
+  }
+
   // Do not allow assignments like 'X = X'. This will just cause infinite loops
   // in the resolution machinery.
   if (BitList.empty())
@@ -3610,6 +3646,22 @@ bool TGParser::ParseTemplateArgList(Record *CurRec) {
   return false;
 }
 
+/// ParseOptionalLetMode - Check for optional 'append' or 'prepend' contextual
+/// keyword after 'let'. Returns the LetMode and consumes the token if found.
+/// The current token must be tgtok::Id before calling.
+LetMode TGParser::ParseOptionalLetMode() {
+  assert(Lex.getCode() == tgtok::Id);
+  if (Lex.getCurStrVal() == "append") {
+    Lex.Lex();
+    return LetMode::Append;
+  }
+  if (Lex.getCurStrVal() == "prepend") {
+    Lex.Lex();
+    return LetMode::Prepend;
+  }
+  return LetMode::Replace;
+}
+
 /// ParseBodyItem - Parse a single item within the body of a def or class.
 ///
 ///   BodyItem ::= Declaration ';'
@@ -3637,8 +3689,15 @@ bool TGParser::ParseBodyItem(Record *CurRec) {
     return false;
   }
 
-  // LET ID OptionalRangeList '=' Value ';'
-  if (Lex.Lex() != tgtok::Id)
+  // LET [append|prepend] ID OptionalBitList '=' Value ';'
+  Lex.Lex(); // eat 'let'.
+
+  if (Lex.getCode() != tgtok::Id)
+    return TokError("expected field identifier after let");
+
+  LetMode Mode = ParseOptionalLetMode();
+
+  if (Lex.getCode() != tgtok::Id)
     return TokError("expected field identifier after let");
 
   SMLoc IdLoc = Lex.getLoc();
@@ -3671,7 +3730,8 @@ bool TGParser::ParseBodyItem(Record *CurRec) {
   if (!consume(tgtok::semi))
     return TokError("expected ';' after let expression");
 
-  return SetValue(CurRec, IdLoc, FieldName, BitList, Val);
+  return SetValue(CurRec, IdLoc, FieldName, BitList, Val,
+                  /*AllowSelfAssignment=*/false, /*OverrideDefLoc=*/true, Mode);
 }
 
 /// ParseBody - Read the body of a class or def. Return true on error, false on
@@ -3711,7 +3771,9 @@ bool TGParser::ParseBody(Record *CurRec) {
 bool TGParser::ApplyLetStack(Record *CurRec) {
   for (SmallVectorImpl<LetRecord> &LetInfo : LetStack)
     for (LetRecord &LR : LetInfo)
-      if (SetValue(CurRec, LR.Loc, LR.Name, LR.Bits, LR.Value))
+      if (SetValue(CurRec, LR.Loc, LR.Name, LR.Bits, LR.Value,
+                   /*AllowSelfAssignment=*/false, /*OverrideDefLoc=*/true,
+                   LR.Mode))
         return true;
   return false;
 }
@@ -4187,7 +4249,7 @@ bool TGParser::ParseClass() {
 /// of LetRecords.
 ///
 ///   LetList ::= LetItem (',' LetItem)*
-///   LetItem ::= ID OptionalRangeList '=' Value
+///   LetItem ::= [append|prepend] ID OptionalRangeList '=' Value
 ///
 void TGParser::ParseLetList(SmallVectorImpl<LetRecord> &Result) {
   do {
@@ -4197,6 +4259,14 @@ void TGParser::ParseLetList(SmallVectorImpl<LetRecord> &Result) {
       return;
     }
 
+    LetMode Mode = ParseOptionalLetMode();
+
+    if (Lex.getCode() != tgtok::Id) {
+      TokError("expected identifier in let definition");
+      Result.clear();
+      return;
+    }
+
     const StringInit *Name = StringInit::get(Records, Lex.getCurStrVal());
     SMLoc NameLoc = Lex.getLoc();
     Lex.Lex(); // Eat the identifier.
@@ -4222,7 +4292,7 @@ void TGParser::ParseLetList(SmallVectorImpl<LetRecord> &Result) {
     }
 
     // Now that we have everything, add the record.
-    Result.emplace_back(Name, Bits, Val, NameLoc);
+    Result.emplace_back(Name, Bits, Val, NameLoc, Mode);
   } while (consume(tgtok::comma));
 }
 
diff --git a/llvm/lib/TableGen/TGParser.h b/llvm/lib/TableGen/TGParser.h
index 09b7d5380695d..e7699d288dfbe 100644
--- a/llvm/lib/TableGen/TGParser.h
+++ b/llvm/lib/TableGen/TGParser.h
@@ -26,13 +26,17 @@ struct MultiClass;
 struct SubClassReference;
 struct SubMultiClassReference;
 
+enum class LetMode { Replace, Append, Prepend };
+
 struct LetRecord {
   const StringInit *Name;
   std::vector<unsigned> Bits;
   const Init *Value;
   SMLoc Loc;
-  LetRecord(const StringInit *N, ArrayRef<unsigned> B, const Init *V, SMLoc L)
-      : Name(N), Bits(B), Value(V), Loc(L) {}
+  LetMode Mode;
+  LetRecord(const StringInit *N, ArrayRef<unsigned> B, const Init *V, SMLoc L,
+            LetMode M = LetMode::Replace)
+      : Name(N), Bits(B), Value(V), Loc(L), Mode(M) {}
 };
 
 /// RecordsEntry - Holds exactly one of a Record, ForeachLoop, or
@@ -223,10 +227,11 @@ class TGParser {
   bool AddValue(Record *TheRec, SMLoc Loc, const RecordVal &RV);
   /// Set the value of a RecordVal within the given record. If `OverrideDefLoc`
   /// is set, the provided location overrides any existing location of the
-  /// RecordVal.
+  /// RecordVal. An optional `Mode` specifies append/prepend concatenation.
   bool SetValue(Record *TheRec, SMLoc Loc, const Init *ValName,
                 ArrayRef<unsigned> BitList, const Init *V,
-                bool AllowSelfAssignment = false, bool OverrideDefLoc = true);
+                bool AllowSelfAssignment = false, bool OverrideDefLoc = true,
+                LetMode Mode = LetMode::Replace);
   bool AddSubClass(Record *Rec, SubClassReference &SubClass);
   bool AddSubClass(RecordsEntry &Entry, SubClassReference &SubClass);
   bool AddSubMultiClass(MultiClass *CurMC,
@@ -270,6 +275,7 @@ class TGParser {
   bool ParseIfBody(MultiClass *CurMultiClass, StringRef Kind);
   bool ParseAssert(MultiClass *CurMultiClass, Record *CurRec = nullptr);
   bool ParseTopLevelLet(MultiClass *CurMultiClass);
+  LetMode ParseOptionalLetMode();
   void ParseLetList(SmallVectorImpl<LetRecord> &Result);
 
   bool ParseObjectBody(Record *CurRec);
diff --git a/llvm/test/TableGen/let-append-bitrange-error.td b/llvm/test/TableGen/let-append-bitrange-error.td
new file mode 100644
index 0000000000000..0f6223cbdccef
--- /dev/null
+++ b/llvm/test/TableGen/let-append-bitrange-error.td
@@ -0,0 +1,13 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+// XFAIL: vg_leak
+
+// Test that 'let append' with bit range produces an error.
+
+class Base {
+  bits<8> flags = 0;
+}
+
+// CHECK: error: cannot use append/prepend with bit range
+def Bad : Base {
+  let append flags{0-3} = 0;
+}
diff --git a/llvm/test/TableGen/let-append-error.td b/llvm/test/TableGen/let-append-error.td
new file mode 100644
index 0000000000000..426ef7e69f8fa
--- /dev/null
+++ b/llvm/test/TableGen/let-append-error.td
@@ -0,0 +1,13 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+// XFAIL: vg_leak
+
+// Test that 'let append' on unsupported types produces an error.
+
+class Base {
+  int count = 0;
+}
+
+// CHECK: error: cannot append to field 'count' of type 'int' (expected list, string, code, or dag)
+def Bad : Base {
+  let append count = 1;
+}
diff --git a/llvm/test/TableGen/let-append.td b/llvm/test/TableGen/let-append.td
new file mode 100644
index 0000000000000..f3557a2ba95cf
--- /dev/null
+++ b/llvm/test/TableGen/let-append.td
@@ -0,0 +1,219 @@
+// RUN: llvm-tblgen %s | FileCheck %s
+// XFAIL: vg_leak
+
+// Test 'let append' and 'let prepend' syntax.
+
+def op;
+
+class Base {
+  list<int> items = [1, 2];
+  string text = "hello";
+  dag d = (op);
+}
+
+class WithCode {
+  code body = [{ int x = 0; }];
+}
+
+class WithUnset {
+  list<int> vals = ?;
+  string msg = ?;
+}
+
+// Multi-level inheritance accumulation.
+class Middle : Base {
+  let append items = [3];
+  let append text = " world";
+}
+
+// Multiple inheritance classes.
+class BaseA {
+  list<int> items = [1, 2];
+  string text = "a";
+}
+
+class BaseB {
+  list<int> items = [3, 4];
+  string text = "b";
+}
+
+// Diamond inheritance classes.
+class DiamondBase {
+  list<int> items = [1];
+}
+
+class Left : DiamondBase {
+  let append items = [2];  // items = [1, 2]
+}
+
+class Right : DiamondBase {
+  let append items = [3];  // items = [1, 3]
+}
+
+// Template argument class.
+class Parameterized<list<int> init> {
+  list<int> items = init;
+}
+
+// Multiclass with append/prepend in body.
+multiclass MC<list<int> extra> {
+  def _a : Base {
+    let append items = extra;
+  }
+  def _b : Base {
+    let prepend items = extra;
+  }
+}
+
+// Test that 'append' and 'prepend' can be used as field names
+// (contextual keywords, not reserved).
+class HasAppendField {
+  list<int> append = [1, 2];
+  list<int> prepend = [3, 4];
+}
+
+// --- Definitions (CHECK lines in alphabetical order of def names) ---
+
+// CHECK: def AppendCode
+// CHECK: code body = [{ int x = 0;  int y = 1; }]
+def AppendCode : WithCode {
+  let append body = [{ int y = 1; }];
+}
+
+// CHECK: def AppendDag
+// CHECK: dag d = (op 3:$a);
+def AppendDag : Base {
+  let append d = (op 3:$a);
+}
+
+// CHECK: def AppendList
+// CHECK: list<int> items = [1, 2, 3, 4];
+def AppendList : Base {
+  let append items = [3, 4];
+}
+
+// CHECK: def AppendString
+// CHECK: string text = "hello world";
+def AppendString : Base {
+  let append text = " world";
+}
+
+// CHECK: def AppendUnset
+// CHECK: list<int> vals = [1];
+// CHECK: string msg = "hi";
+def AppendUnset : WithUnset {
+  let append vals = [1];
+  let append msg = "hi";
+}
+
+// Test sequential append + prepend on the same field.
+// CHECK: def Both
+// CHECK: list<int> items = [0, 1, 2, 3];
+def Both : Base {
+  let append items = [3];
+  let prepend items = [0];
+}
+
+// CHECK: def ContextualKeyword
+// CHECK: list<int> append = [1, 2, 5];
+// CHECK: list<int> prepend = [0, 3, 4];
+def ContextualKeyword : HasAppendField {
+  let append append = [5];
+  let prepend prepend = [0];
+}
+
+// Test diamond inheritance: Right is the last parent, so only Right's
+// accumulated value survives. Left's append is lost.
+// CHECK: def Diamond
+// CHECK: list<int> items = [1, 3, 4];
+def Diamond : Left, Right {
+  let append items = [4];
+}
+
+// Test let append on a field set by a template argument.
+// CHECK: def FromTemplateArg
+// CHECK: list<int> items = [1, 2, 3];
+def FromTemplateArg : Parameterized<[1, 2]> {
+  let append items = [3];
+}
+
+// Test let append in multiclass body with defm.
+// CHECK: def MCTest_a
+// CHECK: list<int> items = [1, 2, 10, 20];
+// CHECK: def MCTest_b
+// CHECK: list<int> items = [10, 20, 1, 2];
+defm MCTest : MC<[10, 20]>;
+
+// Test multiple inheritance: last parent class wins, then append applies.
+// CHECK: def MultiInherit
+// CHECK: list<int> items = [3, 4, 5];
+// CHECK: string text = "b!";
+def MultiInherit : BaseA, BaseB {
+  let append items = [5];
+  let append text = "!";
+}
+
+// CHECK: def MultiLevel
+// CHECK: list<int> items = [1, 2, 3, 4];
+// CHECK: string text = "hello world!";
+def MultiLevel : Middle {
+  let append items = [4];
+  let append text = "!";
+}
+
+// Test nested top-level let append.
+let append items = [100] in {
+  let append items = [200] in {
+    // CHECK: def NestedTopLevel
+    // CHECK: list<int> items = [1, 2, 100, 200];
+    def NestedTopLevel : Base;
+  }
+}
+
+// CHECK: def OverrideAfterAppend
+// CHECK: list<int> items = [10];
+def OverrideAfterAppend : Base {
+  let append items = [3];
+  let items = [10];
+}
+
+// CHECK: def PrependDag
+// CHECK: dag d = (op 0:$z);
+def PrependDag : Base {
+  let prepend d = (op 0:$z);
+}
+
+// CHECK: def PrependList
+// CHECK: list<int> items = [0, 1, 2];
+def PrependList : Base {
+  let prepend items = [0];
+}
+
+// CHECK: def PrependString
+// CHECK: string text = "say hello";
+def PrependString : Base {
+  let prepend text = "say ";
+}
+
+// Test prepend on unset fields.
+// CHECK: def PrependUnset
+// CHECK: list<int> vals = [1];
+// CHECK: string msg = "hi";
+def PrependUnset : WithUnset {
+  let prepend vals = [1];
+  let prepend msg = "hi";
+}
+
+// Test top-level let with append.
+let append items = [100] in {
+  // CHECK: def TopLevelAppend
+  // CHECK: list<int> items = [1, 2, 100];
+  def TopLevelAppend : Base;
+}
+
+// Test top-level let with prepend.
+let prepend items = [0] in {
+  // CHECK: def TopLevelPrepend
+  // CHECK: list<int> items = [0, 1, 2];
+  def TopLevelPrepend : Base;
+}
diff --git a/llvm/test/TableGen/let-prepend-error.td b/llvm/test/TableGen/let-prepend-error.td
new file mode 100644
index 0000000000000..02ee08cb35350
--- /dev/null
+++ b/llvm/test/TableGen/let-prepend-error.td
@@ -0,0 +1,13 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+// XFAIL: vg_leak
+
+// Test that 'let prepend' on unsupported types produces an error.
+
+class Base {
+  int count = 0;
+}
+
+// CHECK: error: cannot prepend to field 'count' of type 'int' (expected list, string, code, or dag)
+def Bad : Base {
+  let prepend count = 1;
+}
diff --git a/mlir/docs/DefiningDialects/AttributesAndTypes.md b/mlir/docs/DefiningDialects/AttributesAndTypes.md
index 5d9744462a4b1..23caeb0d6b65c 100644
--- a/mlir/docs/DefiningDialects/AttributesAndTypes.md
+++ b/mlir/docs/DefiningDialects/AttributesAndTypes.md
@@ -1200,6 +1200,30 @@ Note that these are mechanisms intended for long-tail cases by power users; for
 not-yet-implemented widely-applicable cases, improving the infrastructure is
 preferable.
 
+### Accumulating extra declarations with `let append`
+
+Similar to [operations](Operations.md#accumulating-extra-declarations-with-let-append),
+attribute and type definitions support `let append` on `extraClassDeclaration`
+and `extraClassDefinition`. These values accumulate across the TableGen
+class hierarchy, so base classes can provide shared C++ code that is
+automatically included in all derived attributes or types. A derived class can
+opt out by using a plain `let` to override the accumulated value.
+
+```tablegen
+class MyBaseType<string name> : TypeDef<MyDialect, name> {
+  let append extraClassDeclaration = [{
+    bool isCompatible(Type other);
+  }];
+  let append extraClassDefinition = [{
+    bool $cppClass::isCompatible(Type other) { return other == *this; }
+  }];
+}
+
+def FooType : MyBaseType<"Foo"> {
+  // FooType gets isCompatible() automatically.
+}
+```
+
 ### Mnemonic Alias in Assembly
 
 Attribute and Type can use aliases in the assembly to reduce verbosity.
diff --git a/mlir/docs/DefiningDialects/Operations.md b/mlir/docs/DefiningDialects/Operations.md
index 08fb08998437e..183e9ed519f8d 100644
--- a/mlir/docs/DefiningDialects/Operations.md
+++ b/mlir/docs/DefiningDialects/Operations.md
@@ -1161,6 +1161,40 @@ declaration. In these cases, users can add an `extraClassDefinition` to define
 code that is added to the generated source file inside the op's C++ namespace.
 The substitution `$cppClass` is replaced by the op's C++ class name.
 
+### Accumulating extra declarations with `let append`
+
+When defining base op classes in TableGen, `extraClassDeclaration` and
+`extraClassDefinition` follow standard TableGen `let` override semantics: if a
+derived class sets them, the base class values are lost. To provide shared C++
+code that **accumulates** across the class hierarchy, use `let append`:
+
+```tablegen
+class MyDialectOp<string mnemonic, list<Trait> traits = []>
+    : Op<MyDialect, mnemonic, traits> {
+  let append extraClassDeclaration = [{
+    MyDialect &getDialectInstance();
+  }];
+  let append extraClassDefinition = [{
+    MyDialect &$cppClass::getDialectInstance() {
+      return static_cast<MyDialect &>((*this)->getDialect());
+    }
+  }];
+}
+
+def FooOp : MyDialectOp<"foo"> {
+  // FooOp gets both getDialectInstance() and doFoo().
+  let append extraClassDeclaration = [{ void doFoo(); }];
+}
+
+def BarOp : MyDialectOp<"bar"> {
+  // BarOp gets getDialectInstance() automatically.
+}
+```
+
+Multiple levels of the hierarchy can use `let append extraClassDeclaration`;
+all their values are concatenated in the generated code. A derived class can
+opt out by using a plain `let` to override the accumulated value.
+
 ### Generated C++ code
 
 [OpDefinitionsGen][OpDefinitionsGen] processes the op definition spec file and
diff --git a/mlir/test/mlir-tblgen/attrdefs.td b/mlir/test/mlir-tblgen/attrdefs.td
index a809611fd0aec..ae171b84a84f8 100644
--- a/mlir/test/mlir-tblgen/attrdefs.td
+++ b/mlir/test/mlir-tblgen/attrdefs.td
@@ -204,3 +204,85 @@ def J_CustomStorageCtorAttr : AttrDef<Test_Dialect, "CustomStorageCtorAttr"> {
 // DEF-LABEL: struct CustomStorageCtorAttrAttrStorage : public ::mlir::AttributeStorage
 // DEF: static CustomStorageCtorAttrAttrStorage *construct
 // DEF-SAME: (::mlir::AttributeStorageAllocator &allocator, KeyTy &&tblgenKey);
+
+// Test 'let append' for extra class declarations/definitions in attributes.
+
+class InheritableTestAttr<string name> : AttrDef<Test_Dialect, name> {
+  let append extraClassDeclaration = [{
+    int getInheritedHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getInheritedHelper() { return 42; }
+  }];
+}
+
+// Both appended and regular extra declarations should appear.
+def K_InheritableAttrA : InheritableTestAttr<"InheritableA"> {
+  let attrName = "test.inheritable_a";
+  let append extraClassDeclaration = [{
+    void doA();
+  }];
+  let append extraClassDefinition = [{
+    void $cppClass::doA() {}
+  }];
+}
+
+// DECL-LABEL: class InheritableAAttr
+// DECL: int getInheritedHelper();
+// DECL: void doA();
+
+// DEF-LABEL: int InheritableAAttr::getInheritedHelper()
+// DEF: return 42;
+// DEF-LABEL: void InheritableAAttr::doA()
+
+// Only appended declarations from parent (no additional extraClassDeclaration).
+def L_InheritableAttrB : InheritableTestAttr<"InheritableB"> {
+  let attrName = "test.inheritable_b";
+}
+
+// DECL-LABEL: class InheritableBAttr
+// DECL: int getInheritedHelper();
+
+// Discard accumulated declarations by using plain 'let' to override.
+def M_InheritableAttrC : InheritableTestAttr<"InheritableC"> {
+  let attrName = "test.inheritable_c";
+  let extraClassDeclaration = [{}];
+  let extraClassDefinition = [{}];
+}
+
+// DECL-LABEL: class InheritableCAttr
+// DECL-NOT: int getInheritedHelper();
+
+// Middle-of-stack: accumulates with base declarations via 'let append'.
+class InheritableMiddleAttr<string name> : InheritableTestAttr<name> {
+  let append extraClassDeclaration = [{
+    int getMiddleHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getMiddleHelper() { return 1; }
+  }];
+}
+
+// Concrete attr inheriting from middle gets both base and middle.
+def N_InheritableAttrD : InheritableMiddleAttr<"InheritableD"> {
+  let attrName = "test.inheritable_d";
+}
+
+// DECL-LABEL: class InheritableDAttr
+// DECL: int getInheritedHelper();
+// DECL: int getMiddleHelper();
+
+// DEF-LABEL: int InheritableDAttr::getInheritedHelper()
+// DEF: return 42;
+// DEF-LABEL: int InheritableDAttr::getMiddleHelper()
+// DEF: return 1;
+
+// Passthrough: middle class doesn't append, base value passes through.
+class InheritablePassthroughAttr<string name> : InheritableTestAttr<name> {}
+
+def O_InheritableAttrE : InheritablePassthroughAttr<"InheritableE"> {
+  let attrName = "test.inheritable_e";
+}
+
+// DECL-LABEL: class InheritableEAttr
+// DECL: int getInheritedHelper();
diff --git a/mlir/test/mlir-tblgen/op-decl-and-defs.td b/mlir/test/mlir-tblgen/op-decl-and-defs.td
index 80dedb8475b9e..9449c7b83ea48 100644
--- a/mlir/test/mlir-tblgen/op-decl-and-defs.td
+++ b/mlir/test/mlir-tblgen/op-decl-and-defs.td
@@ -360,6 +360,43 @@ def NS_IOp : NS_Op<"op_with_same_operands_and_result_types_trait", [SameOperands
 // CHECK: static IOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::ValueRange operands, const Properties &properties, ::llvm::ArrayRef<::mlir::NamedAttribute> discardableAttributes = {});
 // CHECK: static IOp create(::mlir::ImplicitLocOpBuilder &builder, ::mlir::ValueRange operands, const Properties &properties, ::llvm::ArrayRef<::mlir::NamedAttribute> discardableAttributes = {});
 
+// CHECK-LABEL: NS::InheritableOpA declarations
+// CHECK: int getInheritedHelper();
+// CHECK: void doA();
+
+// CHECK-LABEL: NS::InheritableOpB declarations
+// CHECK: int getInheritedHelper();
+
+// Discard: no inheritable declarations.
+// CHECK-LABEL: NS::InheritableOpC declarations
+// CHECK-NOT: int getInheritedHelper();
+
+// Middle-of-stack: gets both base and middle inheritable declarations.
+// CHECK-LABEL: NS::InheritableOpD declarations
+// CHECK: int getInheritedHelper();
+// CHECK: int getMiddleHelper();
+
+// Middle-of-stack with own extraClassDeclaration.
+// CHECK-LABEL: NS::InheritableOpE declarations
+// CHECK: int getInheritedHelper();
+// CHECK: int getMiddleHelper();
+// CHECK: void doE();
+
+// Passthrough: middle class doesn't set inheritable, base value passes through.
+// CHECK-LABEL: NS::InheritableOpF declarations
+// CHECK: int getInheritedHelper();
+
+// DEFS-LABEL: NS::InheritableOpA definitions
+// DEFS: int InheritableOpA::getInheritedHelper() { return 42; }
+// DEFS: void InheritableOpA::doA() {}
+
+// DEFS-LABEL: NS::InheritableOpD definitions
+// DEFS: int InheritableOpD::getInheritedHelper() { return 42; }
+// DEFS: int InheritableOpD::getMiddleHelper() { return 1; }
+
+// DEFS-LABEL: NS::InheritableOpF definitions
+// DEFS: int InheritableOpF::getInheritedHelper() { return 42; }
+
 // Check default value of `attributes` for the `genInferredTypeCollectiveParamBuilder` builder
 def NS_JOp : NS_Op<"op_with_InferTypeOpInterface_interface", [DeclareOpInterfaceMethods<InferTypeOpInterface>]> {
   let arguments = (ins AnyType:$a, AnyType:$b);
@@ -551,3 +588,64 @@ def _TypeInferredPropOp : NS_Op<"type_inferred_prop_op_with_properties", [
   let results = (outs AnyType:$result);
   let hasCustomAssemblyFormat = 1;
 }
+
+// Test 'let append' for extra class declarations/definitions.
+class NS_InheritableOp<string mnemonic, list<Trait> traits = []>
+    : NS_Op<mnemonic, traits> {
+  let append extraClassDeclaration = [{
+    int getInheritedHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getInheritedHelper() { return 42; }
+  }];
+}
+
+// Both appended and regular extra declarations should appear.
+def NS_InheritableOpA : NS_InheritableOp<"inheritable_op_a"> {
+  let append extraClassDeclaration = [{
+    void doA();
+  }];
+  let append extraClassDefinition = [{
+    void $cppClass::doA() {}
+  }];
+}
+
+// Only appended declarations from parent (no additional extraClassDeclaration).
+def NS_InheritableOpB : NS_InheritableOp<"inheritable_op_b"> {}
+
+// Discard accumulated declarations by using plain 'let' to override.
+def NS_InheritableOpC : NS_InheritableOp<"inheritable_op_c"> {
+  let extraClassDeclaration = ?;
+  let extraClassDefinition = ?;
+}
+
+// Middle-of-stack: NS_Op -> NS_InheritableOp -> NS_InheritableMiddleOp
+// The middle class appends its own extraClassDeclaration. Concrete ops
+// get both the base and middle declarations (accumulated via 'let append').
+class NS_InheritableMiddleOp<string mnemonic, list<Trait> traits = []>
+    : NS_InheritableOp<mnemonic, traits> {
+  let append extraClassDeclaration = [{
+    int getMiddleHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getMiddleHelper() { return 1; }
+  }];
+}
+
+// Concrete op inheriting from middle class gets both base and middle.
+def NS_InheritableOpD : NS_InheritableMiddleOp<"inheritable_op_d"> {}
+
+// Concrete op inheriting from middle class with its own appended declaration.
+def NS_InheritableOpE : NS_InheritableMiddleOp<"inheritable_op_e"> {
+  let append extraClassDeclaration = [{
+    void doE();
+  }];
+}
+
+// Middle class that does NOT append — inherits the base class value and
+// passes it through to concrete ops.
+class NS_InheritablePassthroughOp<string mnemonic, list<Trait> traits = []>
+    : NS_InheritableOp<mnemonic, traits> {}
+
+// Concrete op gets the original base class declarations.
+def NS_InheritableOpF : NS_InheritablePassthroughOp<"inheritable_op_f"> {}
diff --git a/mlir/test/mlir-tblgen/typedefs.td b/mlir/test/mlir-tblgen/typedefs.td
index b9e3a7954e361..d899d0bfa47f1 100644
--- a/mlir/test/mlir-tblgen/typedefs.td
+++ b/mlir/test/mlir-tblgen/typedefs.td
@@ -153,3 +153,85 @@ def E_IntegerType : TestType<"Integer"> {
 // DECL-NEXT: /// Return true if this is an unsigned integer type.
 // DECL-NEXT: bool isUnsigned() const { return getSignedness() == Unsigned; }
 }
+
+// Test 'let append' for extra class declarations/definitions in types.
+
+class InheritableTestType<string name> : TypeDef<Test_Dialect, name> {
+  let append extraClassDeclaration = [{
+    int getInheritedHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getInheritedHelper() { return 42; }
+  }];
+}
+
+// Both appended and regular extra declarations should appear.
+def F_InheritableTypeA : InheritableTestType<"InheritableA"> {
+  let typeName = "test.inheritable_a";
+  let append extraClassDeclaration = [{
+    void doA();
+  }];
+  let append extraClassDefinition = [{
+    void $cppClass::doA() {}
+  }];
+}
+
+// DECL-LABEL: class InheritableAType
+// DECL: int getInheritedHelper();
+// DECL: void doA();
+
+// DEF-LABEL: int InheritableAType::getInheritedHelper()
+// DEF: return 42;
+// DEF-LABEL: void InheritableAType::doA()
+
+// Only appended declarations from parent (no additional extraClassDeclaration).
+def G_InheritableTypeB : InheritableTestType<"InheritableB"> {
+  let typeName = "test.inheritable_b";
+}
+
+// DECL-LABEL: class InheritableBType
+// DECL: int getInheritedHelper();
+
+// Discard accumulated declarations by using plain 'let' to override.
+def H_InheritableTypeC : InheritableTestType<"InheritableC"> {
+  let typeName = "test.inheritable_c";
+  let extraClassDeclaration = [{}];
+  let extraClassDefinition = [{}];
+}
+
+// DECL-LABEL: class InheritableCType
+// DECL-NOT: int getInheritedHelper();
+
+// Middle-of-stack: accumulates with base declarations via 'let append'.
+class InheritableMiddleType<string name> : InheritableTestType<name> {
+  let append extraClassDeclaration = [{
+    int getMiddleHelper();
+  }];
+  let append extraClassDefinition = [{
+    int $cppClass::getMiddleHelper() { return 1; }
+  }];
+}
+
+// Concrete type inheriting from middle gets both base and middle.
+def I_InheritableTypeD : InheritableMiddleType<"InheritableD"> {
+  let typeName = "test.inheritable_d";
+}
+
+// DECL-LABEL: class InheritableDType
+// DECL: int getInheritedHelper();
+// DECL: int getMiddleHelper();
+
+// DEF-LABEL: int InheritableDType::getInheritedHelper()
+// DEF: return 42;
+// DEF-LABEL: int InheritableDType::getMiddleHelper()
+// DEF: return 1;
+
+// Passthrough: middle class doesn't append, base value passes through.
+class InheritablePassthroughType<string name> : InheritableTestType<name> {}
+
+def J_InheritableTypeE : InheritablePassthroughType<"InheritableE"> {
+  let typeName = "test.inheritable_e";
+}
+
+// DECL-LABEL: class InheritableEType
+// DECL: int getInheritedHelper();



More information about the Mlir-commits mailing list