[llvm] 89d150a - [TableGen] Add let append/prepend syntax for field concatenation (#182382)

via llvm-commits llvm-commits at lists.llvm.org
Mon Mar 9 10:54:13 PDT 2026


Author: Henrich Lauko
Date: 2026-03-09T18:54:08+01:00
New Revision: 89d150a7973d20dced91bb3c87be95879564b6b5

URL: https://github.com/llvm/llvm-project/commit/89d150a7973d20dced91bb3c87be95879564b6b5
DIFF: https://github.com/llvm/llvm-project/commit/89d150a7973d20dced91bb3c87be95879564b6b5.diff

LOG: [TableGen] Add let append/prepend syntax for field concatenation (#182382)

## Motivation

LLVM TableGen currently lacks a way to **accumulate** field values
across class hierarchies. When a derived class sets a field via `let`,
it completely replaces the parent's value. This forces users into
verbose workarounds like:

```tablegen
class Op { // This is generic MLIR Base 
  code extraClassDeclaration = ?;
}

// Some Generic shared base
class MyShared1OpClass : Op {
  code shared1ExtraClassDeclaration = [{ some generic code 1 }];
}

class MyShared2OpClass : MyShared1OpClass {
  code shared2ExtraClassDeclaration = [{ some generic code 2 }];
}

def MyOp : MyShared2OpClass {
  // need to manually concatenate shared code
  let extraClassDeclaration =   
      shared1ExtraClassDeclaration
    # shared2ExtraClassDeclaration
    # [{ additional specialized code }]; 
}
```

Instead I propose a more natural incremental solution without
unnecessery intermediate definitions:

```
class Op {
  code extraClassDeclaration = ?;
}

class MyShared1OpClass : Op {
  let append extraClassDeclaration = [{ some generic code 1 }];
}

class MyShared2OpClass : MyShared1OpClass {
  let append extraClassDeclaration = [{ some generic code 2 }];
}

def MyOp : MyShared2OpClass {
  let append extraClassDeclaration = [{ additional specialized code }]; 
}
```

This is especially painful in MLIR, where dialect authors want base
op/type/attribute classes to inject shared C++ declarations into all
derived definitions. I attempted to solve this in PR
https://github.com/llvm/llvm-project/pull/182265 with MLIR-specific
`inheritableExtraClassDeclaration`/`Definition` fields, but as
@joker-eph [pointed
out](https://github.com/llvm/llvm-project/pull/182265#discussion_r2098718600),
this is ad-hoc -- the same inheritance problem exists for `traits`,
`arguments`, `results`, and any other list/string/dag field. Rather than
adding `inheritable*` variants per field, we should solve this at the
language level.

## Design

This PR adds two new modifiers to the `let` statement: **`append`** and
**`prepend`**.

```tablegen
class Base {
  list<int> items = [1, 2];
  string text = "hello";
  dag d = (op);
}

def Example : Base {
  let append items = [3, 4];    // items = [1, 2, 3, 4]
  let prepend items = [0];      // items = [0, 1, 2]
  let append text = " world";   // text = "hello world"
  let prepend text = "say ";    // text = "say hello"
  let append d = (op 3:$a);     // d = (op 3:$a)
}
```

### Supported types

| Field type | Operation | Concat operator |
|---|---|---|
| `list<T>` | append/prepend | `!listconcat` |
| `string` / `code` | append/prepend | `!strconcat` |
| `dag` | append/prepend | `!con` |
| Other (`bit`, `int`, `bits`) | -- | Error |

### Semantics

- **`let append`** concatenates the new value **after** the current
value
- **`let prepend`** concatenates the new value **before** the current
value
- If the current value is **unset** (`?`), the new value is used
directly
- A plain **`let`** (without modifier) still replaces, allowing opt-out
from accumulated values
- Works in both **body-level** (`def Foo { let append ... }`) and
**top-level** (`let append ... in { }`) contexts

### Multi-level inheritance

Accumulation works naturally across inheritance chains:

```tablegen
class Base {
  list<int> items = [1, 2];
}

class Middle : Base {
  let append items = [3];    // items = [1, 2, 3]
}

def Leaf : Middle {
  let append items = [4];    // items = [1, 2, 3, 4]
}
```

### Multiple inheritance

TableGen supports multiple inheritance (`def D : A, B { ... }`), where
parent classes are processed left to right and the **last parent class's
value wins** for any shared field. `let append`/`let prepend` operates
on whatever value the field has *after* inheritance resolution — it does
not accumulate across sibling parents:

```tablegen
class A { list<int> items = [1, 2]; }
class B { list<int> items = [3, 4]; }

def D : A, B {
  let append items = [5];  // items = [3, 4, 5]  (A's value is lost)
}
```

This also applies to diamond inheritance:

```tablegen
class Base  { list<int> items = [1]; }
class Left  : Base { let append items = [2]; }  // [1, 2]
class Right : Base { let append items = [3]; }  // [1, 3]

def D : Left, Right {
  let append items = [4];  // items = [1, 3, 4]  (Left's [2] is lost)
}
```

This is consistent with how plain `let` works with multiple inheritance
— it is the standard last-writer-wins rule. Users who need accumulation
from multiple parents should use a single-inheritance chain instead.

## Backward compatibility

This proposal is **fully backward compatible**. The keywords `append`
and `prepend` are implemented as **context-sensitive keywords** — they
are only recognized as modifiers when they appear immediately after
`let` (in both body-level and top-level contexts). In all other
positions, `append` and `prepend` remain valid identifiers and can be
used as field names, class names, def names, etc. This means:

- No existing `.td` files (in-tree or out-of-tree) will break
- Fields named `append` or `prepend` continue to work: `let append
append = [5];` is valid (the first `append` is the modifier, the second
is the field name)
- The parser checks for the identifier string value after `let`, not for
a reserved token

RFC:
https://discourse.llvm.org/t/rfc-tablegen-add-let-append-prepend-syntax-for-field-concatenation/89924/

Added: 
    llvm/test/TableGen/let-append-bitrange-error.td
    llvm/test/TableGen/let-append-error.td
    llvm/test/TableGen/let-append-toplevel.td
    llvm/test/TableGen/let-append.td
    llvm/test/TableGen/let-prepend-error.td

Modified: 
    llvm/docs/TableGen/ProgRef.rst
    llvm/lib/TableGen/TGParser.cpp
    llvm/lib/TableGen/TGParser.h

Removed: 
    


################################################################################
diff  --git a/llvm/docs/TableGen/ProgRef.rst b/llvm/docs/TableGen/ProgRef.rst
index 2e66778b42ae1..7c041ec530d03 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 :token:`BodyItem` production 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..74a9c789c9859 100644
--- a/llvm/lib/TableGen/TGParser.cpp
+++ b/llvm/lib/TableGen/TGParser.cpp
@@ -14,6 +14,7 @@
 #include "TGLexer.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringSwitch.h"
 #include "llvm/ADT/Twine.h"
 #include "llvm/Config/llvm-config.h"
 #include "llvm/Support/Casting.h"
@@ -238,7 +239,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 +252,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,10 +3647,47 @@ bool TGParser::ParseTemplateArgList(Record *CurRec) {
   return false;
 }
 
+/// Parse an optional 'append'/'prepend' mode followed by a field name.
+///
+/// The current token must be an identifier. If the identifier is 'append' or
+/// 'prepend' and is followed by another identifier, it is interpreted as a
+/// mode keyword and the following identifier is parsed as the field name.
+/// Otherwise the identifier itself is treated as the field name.
+///
+/// These keywords are contextual: a field may still be named 'append' or
+/// 'prepend' (e.g. `let append = ...`). In that case the keyword is not
+/// interpreted as a mode and the identifier is parsed as the field name.
+LetModeAndName TGParser::ParseLetModeAndName() {
+  assert(Lex.getCode() == tgtok::Id && "expected identifier");
+
+  SMLoc Loc = Lex.getLoc();
+  // Copy the identifier before Lex.Lex() invalidates the lexer buffer.
+  std::string CurStr = Lex.getCurStrVal();
+
+  LetMode Mode = llvm::StringSwitch<LetMode>(CurStr)
+                     .Case("append", LetMode::Append)
+                     .Case("prepend", LetMode::Prepend)
+                     .Default(LetMode::Replace);
+
+  // Consume the current identifier.
+  Lex.Lex();
+
+  if (Mode != LetMode::Replace && Lex.getCode() == tgtok::Id) {
+    // 'append'/'prepend' used as a contextual keyword.
+    LetModeAndName Result = {Mode, Lex.getLoc(), Lex.getCurStrVal()};
+    Lex.Lex(); // Consume the field name.
+    return Result;
+  }
+
+  // Otherwise the identifier itself is the field name (including the case
+  // where the field is literally named 'append' or 'prepend').
+  return {LetMode::Replace, Loc, std::move(CurStr)};
+}
+
 /// ParseBodyItem - Parse a single item within the body of a def or class.
 ///
 ///   BodyItem ::= Declaration ';'
-///   BodyItem ::= LET ID OptionalBitList '=' Value ';'
+///   BodyItem ::= LET [append|prepend] ID OptionalBitList '=' Value ';'
 ///   BodyItem ::= Defvar
 ///   BodyItem ::= Dump
 ///   BodyItem ::= Assert
@@ -3637,13 +3711,14 @@ 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");
 
-  SMLoc IdLoc = Lex.getLoc();
-  const StringInit *FieldName = StringInit::get(Records, Lex.getCurStrVal());
-  Lex.Lex(); // eat the field name.
+  auto [Mode, IdLoc, FieldNameStr] = ParseLetModeAndName();
+  const StringInit *FieldName = StringInit::get(Records, FieldNameStr);
 
   SmallVector<unsigned, 16> BitList;
   if (ParseOptionalBitList(BitList))
@@ -3671,7 +3746,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 +3787,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 +4265,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,9 +4275,8 @@ void TGParser::ParseLetList(SmallVectorImpl<LetRecord> &Result) {
       return;
     }
 
-    const StringInit *Name = StringInit::get(Records, Lex.getCurStrVal());
-    SMLoc NameLoc = Lex.getLoc();
-    Lex.Lex(); // Eat the identifier.
+    auto [Mode, NameLoc, NameStr] = ParseLetModeAndName();
+    const StringInit *Name = StringInit::get(Records, NameStr);
 
     // Check for an optional RangeList.
     SmallVector<unsigned, 16> Bits;
@@ -4222,7 +4299,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..9f0b89f080c9e 100644
--- a/llvm/lib/TableGen/TGParser.h
+++ b/llvm/lib/TableGen/TGParser.h
@@ -26,13 +26,29 @@ struct MultiClass;
 struct SubClassReference;
 struct SubMultiClassReference;
 
+/// Specifies how a 'let' assignment interacts with the existing field value.
+/// - Replace: overwrite the field (default behavior).
+/// - Append: concatenate the new value after the existing value.
+/// - Prepend: concatenate the new value before the existing value.
+enum class LetMode { Replace, Append, Prepend };
+
+/// Parsed let mode keyword and field name (e.g. `let append x` yields
+/// Mode=Append, Name="x"; plain `let x` yields Mode=Replace, Name="x").
+struct LetModeAndName {
+  LetMode Mode;
+  SMLoc Loc;        // Source location of the field name.
+  std::string Name; // The field name being assigned.
+};
+
 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 +239,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 +287,7 @@ class TGParser {
   bool ParseIfBody(MultiClass *CurMultiClass, StringRef Kind);
   bool ParseAssert(MultiClass *CurMultiClass, Record *CurRec = nullptr);
   bool ParseTopLevelLet(MultiClass *CurMultiClass);
+  LetModeAndName ParseLetModeAndName();
   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..b0b912d7b34bb
--- /dev/null
+++ b/llvm/test/TableGen/let-append-bitrange-error.td
@@ -0,0 +1,12 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+
+// 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..904b5abbdca4d
--- /dev/null
+++ b/llvm/test/TableGen/let-append-error.td
@@ -0,0 +1,12 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+
+// 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-toplevel.td b/llvm/test/TableGen/let-append-toplevel.td
new file mode 100644
index 0000000000000..4ecda52296aa4
--- /dev/null
+++ b/llvm/test/TableGen/let-append-toplevel.td
@@ -0,0 +1,63 @@
+// RUN: llvm-tblgen %s | FileCheck %s
+
+// Test 'let append' and 'let prepend' syntax in top-level let.
+
+class Base {
+  list<int> items = [1, 2];
+  string text = "hello";
+}
+
+// Test that 'append' and 'prepend' can be used as field names in top-level let.
+class HasAppendField {
+  list<int> append = [1, 2];
+  list<int> prepend = [3, 4];
+  int other = 0;
+}
+
+// Test top-level let with multiple items: no mode, append, and prepend.
+// CHECK: def MultiItemTopLevel
+// CHECK: list<int> items = [0, 1, 2, 100];
+// CHECK: string text = "prefix hello suffix";
+let prepend items = [0], append items = [100],
+    prepend text = "prefix ", append text = " suffix" in {
+  def MultiItemTopLevel : Base;
+}
+
+// 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;
+  }
+}
+
+// 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 append = ...' where 'append' is the field name.
+let append = [10, 20] in {
+  // CHECK: def TopLevelFieldNamedAppend
+  // CHECK: list<int> append = [10, 20];
+  // CHECK: list<int> prepend = [3, 4];
+  def TopLevelFieldNamedAppend : HasAppendField;
+}
+
+// Test top-level 'let prepend = ...' where 'prepend' is the field name.
+let prepend = [30, 40] in {
+  // CHECK: def TopLevelFieldNamedPrepend
+  // CHECK: list<int> append = [1, 2];
+  // CHECK: list<int> prepend = [30, 40];
+  def TopLevelFieldNamedPrepend : HasAppendField;
+}
+
+// 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-append.td b/llvm/test/TableGen/let-append.td
new file mode 100644
index 0000000000000..1b811a7e91dac
--- /dev/null
+++ b/llvm/test/TableGen/let-append.td
@@ -0,0 +1,224 @@
+// RUN: llvm-tblgen %s | FileCheck %s
+
+// Test 'let append' and 'let prepend' syntax in body items.
+
+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];
+  int other = 0;
+}
+
+// Test that scalar fields named 'append'/'prepend' work with plain let.
+class HasScalarAppendField {
+  int append = 0;
+  int prepend = 0;
+}
+
+// --- 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 'let append = ...' on a scalar (int) field named 'append'.
+// CHECK: def AssignScalarAppend
+// CHECK: int append = 42;
+// CHECK: int prepend = 99;
+def AssignScalarAppend : HasScalarAppendField {
+  let append = 42;
+  let prepend = 99;
+}
+
+// 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];
+}
+
+// Test 'let append append' where the second 'append' is the field name.
+// CHECK: def ContextualKeyword
+// CHECK: list<int> append = [1, 2, 5];
+// CHECK: list<int> prepend = [0, 3, 4];
+// CHECK: int other = 0;
+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 = ...' where 'append' is the field name (no mode keyword).
+// CHECK: def FieldNamedAppend
+// CHECK: list<int> append = [10, 20];
+// CHECK: list<int> prepend = [30, 40];
+// CHECK: int other = 5;
+def FieldNamedAppend : HasAppendField {
+  let append = [10, 20];
+  let prepend = [30, 40];
+  let other = 5;
+}
+
+// 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 = "!";
+}
+
+// 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";
+}

diff  --git a/llvm/test/TableGen/let-prepend-error.td b/llvm/test/TableGen/let-prepend-error.td
new file mode 100644
index 0000000000000..3a0e96eb95c0b
--- /dev/null
+++ b/llvm/test/TableGen/let-prepend-error.td
@@ -0,0 +1,12 @@
+// RUN: not llvm-tblgen %s 2>&1 | FileCheck %s
+
+// 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;
+}


        


More information about the llvm-commits mailing list