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 (example edited from the original not functional one):
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 [MLIR][TableGen] Add inheritableExtraClassDeclaration/Definition for Op and AttrOrTypeDef by xlauko · Pull Request #182265 · llvm/llvm-project · GitHub with MLIR-specific inheritableExtraClassDeclaration/Definition fields, but as @mehdi_amini pointed out, 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.
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 appendconcatenates the new value after the current valuelet prependconcatenates 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:
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:
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:
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
.tdfiles (in-tree or out-of-tree) will break - Fields named
appendorprependcontinue to work:let append append = [5];is valid (the firstappendis the modifier, the second is the field name) - The parser checks for the identifier string value after
let, not for a reserved token
Implementation in PR [TableGen] Add let append/prepend syntax for field concatenation by xlauko · Pull Request #182382 · llvm/llvm-project · GitHub