Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/Conversion/MooreToCore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ add_circt_conversion_library(CIRCTMooreToCore
CIRCTVerif
CIRCTTransforms
MLIRArithDialect
MLIRArithToLLVM
MLIRControlFlowDialect
MLIRControlFlowToLLVM
MLIRControlFlowTransforms
MLIRFuncDialect
MLIRFuncToLLVM
MLIRLLVMCommonConversion
MLIRSCFDialect
MLIRSCFToControlFlow
MLIRSideEffectInterfaces
Expand Down
195 changes: 187 additions & 8 deletions lib/Conversion/MooreToCore/MooreToCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include "circt/Dialect/Verif/VerifOps.h"
#include "circt/Support/ConversionPatternSet.h"
#include "circt/Transforms/Passes.h"
#include "mlir/Conversion/FuncToLLVM/ConvertFuncToLLVM.h"
#include "mlir/Conversion/LLVMCommon/TypeConverter.h"
#include "mlir/Conversion/SCFToControlFlow/SCFToControlFlow.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
Expand Down Expand Up @@ -56,6 +58,13 @@ struct ClassTypeCache {
LLVM::GlobalOp global;
};

struct VTableInfo {
LLVM::GlobalOp global;
LLVM::LLVMStructType tableTy;
DenseMap<StringRef, unsigned> methodSlot;
SmallVector<SymbolRefAttr> slotTargets;
};

struct ClassStructInfo {
LLVM::LLVMStructType classBody;
LLVM::LLVMStructType headerTy;
Expand Down Expand Up @@ -110,12 +119,23 @@ struct ClassTypeCache {
classToTypeInfoMap[classSym] = info;
}

std::optional<VTableInfo> getVTableInfo(SymbolRefAttr classSym) const {
if (auto it = classToVTableMap.find(classSym); it != classToVTableMap.end())
return it->second;
return std::nullopt;
}

void setVTableInfo(SymbolRefAttr classSym, const VTableInfo &info) {
classToVTableMap[classSym] = info;
}

private:
// Keyed by the SymbolRefAttr of the class.
// Kept private so all accesses are done with helpers which preserve
// invariants
DenseMap<Attribute, ClassStructInfo> classToStructMap;
DenseMap<Attribute, TypeInfoInfo> classToTypeInfoMap;
DenseMap<Attribute, VTableInfo> classToVTableMap;
};

/// Cache for external function declarations. Avoids redundant symbol table
Expand Down Expand Up @@ -234,6 +254,121 @@ getOrCreateTypeInfo(ModuleOp mod, SymbolRefAttr classSym,
cache.setTypeInfo(classSym, info);
return info;
}

static std::string getVTableName(SymbolRefAttr className) {
return className.getRootReference().str() + "::vtable";
}

static void
collectVTableEntries(VTableOp op,
llvm::SmallDenseMap<StringRef, unsigned> &slots,
SmallVector<StringRef> &slotOrder,
SmallVector<SymbolRefAttr> &slotTargets) {
for (Operation &child : op.getBody().front()) {
if (auto nested = dyn_cast<VTableOp>(child)) {
collectVTableEntries(nested, slots, slotOrder, slotTargets);
continue;
}

auto entry = cast<VTableEntryOp>(child);
auto name = entry.getName();
if (auto it = slots.find(name); it != slots.end()) {
slotTargets[it->second] = entry.getTargetAttr();
continue;
}

unsigned idx = slotOrder.size();
slots.insert({name, idx});
slotOrder.push_back(name);
slotTargets.push_back(entry.getTargetAttr());
}
}

static FailureOr<ClassTypeCache::VTableInfo> getOrCreateVTableInfo(
ModuleOp mod, SymbolRefAttr classSym, ConversionPatternRewriter &rewriter,
const LLVMTypeConverter &typeConverter, SymbolTableCollection &symbolTables,
ClassTypeCache &cache) {
if (auto info = cache.getVTableInfo(classSym))
return *info;

auto vtableSym =
SymbolRefAttr::get(classSym.getRootReference(),
FlatSymbolRefAttr::get(mod.getContext(), "vtable"));
VTableOp vtableOp;
for (auto candidate : mod.getOps<VTableOp>()) {
if (candidate.getSymNameAttr() == vtableSym) {
vtableOp = candidate;
break;
}
}
if (!vtableOp)
return failure();

llvm::SmallDenseMap<StringRef, unsigned> slots;
SmallVector<StringRef> slotOrder;
SmallVector<SymbolRefAttr> slotTargets;
collectVTableEntries(vtableOp, slots, slotOrder, slotTargets);

auto ptrTy = LLVM::LLVMPointerType::get(mod.getContext());
SmallVector<Type> slotTypes(slotTargets.size(), ptrTy);
auto tableTy = LLVM::LLVMStructType::getLiteral(mod.getContext(), slotTypes);

auto globalName = getVTableName(classSym);
auto global = mod.lookupSymbol<LLVM::GlobalOp>(globalName);
if (!global) {
OpBuilder builder = OpBuilder::atBlockBegin(mod.getBody());
global = LLVM::GlobalOp::create(
builder, mod.getLoc(), tableTy,
/*isConstant=*/true, LLVM::Linkage::Internal, globalName, Attribute());
global->setAttr("moore.vtable.method_names",
builder.getStrArrayAttr(slotOrder));
global->setAttr("moore.vtable.slot_targets",
builder.getArrayAttr(llvm::to_vector(llvm::map_range(
slotTargets, [&](SymbolRefAttr target) -> Attribute {
return target;
}))));

Block *block = new Block();
global.getInitializerRegion().push_back(block);
builder.setInsertionPointToStart(block);

auto agg =
LLVM::UndefOp::create(builder, mod.getLoc(), tableTy).getResult();
for (auto [idx, target] : llvm::enumerate(slotTargets)) {
auto llvmFunc =
mod.lookupSymbol<LLVM::LLVMFuncOp>(target.getRootReference());
if (!llvmFunc) {
auto funcOp = mod.lookupSymbol<func::FuncOp>(target.getRootReference());
if (!funcOp)
return failure();

OpBuilder::InsertionGuard guard(rewriter);
rewriter.setInsertionPoint(funcOp);
auto converted = convertFuncOpToLLVMFuncOp(
funcOp, rewriter, typeConverter, &symbolTables);
if (failed(converted))
return failure();
llvmFunc = *converted;
rewriter.eraseOp(funcOp);
Comment on lines +345 to +352
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversion from func.func to llvm.func feels a bit weird, because it only converts a handful of functions that happen to be involved in a class vtable. What happens if the function is not a function but a task, or if it contains LLHD and other ops that interact with an event queue? The fact that the LLVM forces us to unravel most of the IR at this early stage is a bit of a red flag -- we might need another dialect layer in between here. (At the CIRCT core dialects we're still dealing with hardware concerns and higher-level simulation semantics; LLVM might be too low level for some of that 🤔.)

Does the llvm.mlir.addressof op enforce that the target is a global variable or a function? If it does, we might want to try and defer lowering to LLVM until a later point, and see if we can't build up vtables with other MLIR dialects instead of LLVM. Maybe the func.constant op could help create SSA values with a function type, which we could store in a struct and later call with func.call_indirect? We'll probably also need an equivalent for tasks/coroutines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good point about the tasks! Hadn't really thought about that yet 🤔
I think the "natural" representation of a task at this level would be a coroutine - in that way there would also be a LLVM structure to represent it.

I had tinkered with staying with func.func for as long as possible but ran into issues with materialising the vtable since I didn't find a way to properly indirect function calls with call.indirect (Hadn't had a look at func.constant, though🤦)

From my point of view this approach also doesn't really have to unravel any IR except for the direct lowering of func.func to llvm.func (mainly so I can get a function pointer to save into a dispatch table). The llvm.func body contains "the same things" a func.func would. In that sense it's definitely worth trying with func.constant, though - would feel like a nicer solution 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳 My suspicion/fear is that once the outer op is an llvm.func, we'll start inheriting constraints from it -- maybe certain terminators no longer work, yield doesn't work (LLVM coroutines aren't really what we want in CIRCT), and things like llvm.call might not work in all contexts. LLVM feels a lot like an egress dialect, which really wants an all-LLVM-or-nothing setup. If we run into trouble with func.call_indirect or the vtables, we could consider extending the Sim dialect with a corresponding construct that then has a trivial lowering to the LLVM you're generating here... WDYT?

Copy link
Copy Markdown
Contributor Author

@Scheremo Scheremo Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if I manage to tickle those func.constant operators juuuust the right way and potentially materialise vtables a bit later (i.e between MooreToCore and convert-to-llvm) we might just get "de Foifer und 's weggli" with this one 👀

We might then consider legalising moore.vtable.* for core dialects, so we have a structure to record table entry order. But I guess that's a very small price to pay and can be worked around.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing hits quite like de Foifer und 's weggli! 😏 5️⃣ 🥖

About that moore.vtable.* legalization: one of the things you could do is place an op in between the LLVM lowering you were targeting in this PR, and the original moore.vtable.* ops. Something like sim.vtable that is just the flat, distilled-down list of functions that can be called, addressed by index. Something that has an almost trivial 1:1 lowering to LLVM. That allows you to hand-off the Moore-level abstraction into something that's closer to the target while we're at the core dialect level, and then there's a final mechanical switch in convert-to-llvm.

}

auto funcAddr = LLVM::AddressOfOp::create(builder, mod.getLoc(), llvmFunc)
.getResult();
agg = LLVM::InsertValueOp::create(
builder, mod.getLoc(), agg, funcAddr,
ArrayRef<int64_t>{static_cast<int64_t>(idx)})
.getResult();
}
LLVM::ReturnOp::create(builder, mod.getLoc(), agg);
}

ClassTypeCache::VTableInfo info{global, tableTy,
DenseMap<StringRef, unsigned>(), slotTargets};
for (auto [idx, name] : llvm::enumerate(slotOrder))
info.methodSlot[name] = idx;
cache.setVTableInfo(classSym, info);
return info;
}
static LogicalResult resolveClassStructBody(ClassDeclOp op,
TypeConverter const &typeConverter,
ClassTypeCache &cache) {
Expand Down Expand Up @@ -1143,6 +1278,31 @@ struct ClassDeclOpConversion : public OpConversionPattern<ClassDeclOp> {
ClassTypeCache &cache; // shared, owned by the pass
};

struct VTableOpConversion : public OpConversionPattern<VTableOp> {
VTableOpConversion(TypeConverter &tc, MLIRContext *ctx, ClassTypeCache &cache,
SymbolTableCollection &symbolTables,
LLVMTypeConverter &llvmTypeConverter)
: OpConversionPattern<VTableOp>(tc, ctx), cache(cache),
symbolTables(symbolTables), llvmTypeConverter(llvmTypeConverter) {}

LogicalResult
matchAndRewrite(VTableOp op, OpAdaptor,
ConversionPatternRewriter &rewriter) const override {
auto classSym = SymbolRefAttr::get(op.getSymNameAttr().getRootReference());
if (failed(getOrCreateVTableInfo(op->getParentOfType<ModuleOp>(), classSym,
rewriter, llvmTypeConverter, symbolTables,
cache)))
return op.emitOpError() << "Failed to create LLVM vtable global";
rewriter.eraseOp(op);
return success();
}

private:
ClassTypeCache &cache;
SymbolTableCollection &symbolTables;
LLVMTypeConverter &llvmTypeConverter;
};

struct VariableOpConversion : public OpConversionPattern<VariableOp> {
using OpConversionPattern::OpConversionPattern;

Expand Down Expand Up @@ -3016,12 +3176,20 @@ static void populateLegality(ConversionTarget &target,

target.addLegalOp<debug::ScopeOp>();

target.addDynamicallyLegalOp<scf::YieldOp, func::CallOp, func::ReturnOp,
UnrealizedConversionCastOp, hw::OutputOp,
hw::InstanceOp, debug::ArrayOp, debug::StructOp,
debug::VariableOp>(
target.addDynamicallyLegalOp<scf::YieldOp, UnrealizedConversionCastOp,
hw::OutputOp, hw::InstanceOp, debug::ArrayOp,
debug::StructOp, debug::VariableOp>(
[&](Operation *op) { return converter.isLegal(op); });

auto isLegalOutsideLLVMFunc = [&](Operation *op) {
if (op->getParentOfType<LLVM::LLVMFuncOp>())
return false;
return converter.isLegal(op);
};
target.addDynamicallyLegalOp<func::CallOp, func::CallIndirectOp,
func::ConstantOp, func::ReturnOp>(
isLegalOutsideLLVMFunc);

target.addDynamicallyLegalOp<scf::IfOp, scf::ForOp, scf::ExecuteRegionOp,
scf::WhileOp, scf::ForallOp>([&](Operation *op) {
return converter.isLegal(op) && !op->getParentOfType<llhd::ProcessOp>();
Expand Down Expand Up @@ -3251,11 +3419,15 @@ static void populateTypeConversion(TypeConverter &typeConverter) {

static void populateOpConversion(ConversionPatternSet &patterns,
TypeConverter &typeConverter,
LLVMTypeConverter &llvmTypeConverter,
ClassTypeCache &classCache,
FunctionCache &funcCache) {
FunctionCache &funcCache,
SymbolTableCollection &symbolTables) {

patterns.add<ClassDeclOpConversion>(typeConverter, patterns.getContext(),
classCache);
patterns.add<VTableOpConversion>(typeConverter, patterns.getContext(),
classCache, symbolTables, llvmTypeConverter);
patterns.add<ClassNewOpConversion>(typeConverter, patterns.getContext(),
classCache, funcCache);
patterns.add<ClassPropertyRefOpConversion>(typeConverter,
Expand Down Expand Up @@ -3453,6 +3625,8 @@ static void populateOpConversion(ConversionPatternSet &patterns,

mlir::populateAnyFunctionOpInterfaceTypeConversionPattern(patterns,
typeConverter);
mlir::populateFuncToLLVMConversionPatterns(llvmTypeConverter, patterns,
&symbolTables);
hw::populateHWModuleLikeTypeConversionPattern(
hw::HWModuleOp::getOperationName(), patterns, typeConverter);
populateSCFToControlFlowConversionPatterns(patterns);
Expand Down Expand Up @@ -3480,20 +3654,25 @@ void MooreToCorePass::runOnOperation() {
MLIRContext &context = getContext();
ModuleOp module = getOperation();
ClassTypeCache classCache;
auto &symbolTable = getAnalysis<SymbolTable>();
FunctionCache funcCache(symbolTable);
auto &symbolTableAnalysis = getAnalysis<SymbolTable>();
FunctionCache funcCache(symbolTableAnalysis);
SymbolTableCollection symbolTables;

IRRewriter rewriter(module);
(void)mlir::eraseUnreachableBlocks(rewriter, module->getRegions());

TypeConverter typeConverter;
populateTypeConversion(typeConverter);
LowerToLLVMOptions options(&context);
LLVMTypeConverter llvmTypeConverter(&context, options);
populateTypeConversion(llvmTypeConverter);

ConversionTarget target(context);
populateLegality(target, typeConverter);

ConversionPatternSet patterns(&context, typeConverter);
populateOpConversion(patterns, typeConverter, classCache, funcCache);
populateOpConversion(patterns, typeConverter, llvmTypeConverter, classCache,
funcCache, symbolTables);
mlir::cf::populateCFStructuralTypeConversionsAndLegality(typeConverter,
patterns, target);

Expand Down
68 changes: 68 additions & 0 deletions test/Conversion/MooreToCore/classes.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
// CHECK-DAG: llvm.mlir.global internal constant @"VirtualC::typeinfo"() {addr_space = 0 : i32} : !llvm.struct<(ptr)> {
// CHECK-DAG: llvm.mlir.zero : !llvm.ptr
// CHECK-DAG: llvm.insertvalue
// CHECK-DAG: llvm.mlir.global internal constant @"tClass::vtable"()
// CHECK-DAG: llvm.mlir.addressof @"tClass::subroutine" : !llvm.ptr
// CHECK-DAG: llvm.mlir.addressof @"testClass::testSubroutine" : !llvm.ptr
// CHECK-DAG: llvm.mlir.global internal constant @"testClass::vtable"()
// CHECK-DAG: llvm.mlir.addressof @"testClass::subroutine" : !llvm.ptr
// CHECK-DAG: llvm.mlir.addressof @"testClass::testSubroutine" : !llvm.ptr

/// Check that a classdecl gets noop'd and handles are lowered to !llvm.ptr

Expand Down Expand Up @@ -159,3 +165,65 @@ moore.class.classdecl @VirtualC {
moore.class.propertydecl @a : !moore.i32
moore.class.methoddecl @f : (!moore.class<@VirtualC>) -> ()
}

/// Check that symbolic vtables lower to LLVM globals and convert only the
/// referenced methods to llvm.func.

// CHECK-LABEL: llvm.func @"testClass::subroutine"(
// CHECK: llvm.return

// CHECK-LABEL: llvm.func @"testClass::testSubroutine"(
// CHECK: llvm.return

// CHECK-LABEL: llvm.func @"tClass::subroutine"(
// CHECK: llvm.return

// CHECK-NOT: moore.vtable
// CHECK-NOT: moore.vtable_entry

moore.class.classdecl @virtualFunctionClass {
moore.class.methoddecl @subroutine : (!moore.class<@virtualFunctionClass>) -> ()
}
moore.class.classdecl @realFunctionClass implements [@virtualFunctionClass] {
moore.class.methoddecl @testSubroutine : (!moore.class<@realFunctionClass>) -> ()
}
moore.class.classdecl @testClass implements [@realFunctionClass] {
moore.class.methoddecl @subroutine -> @"testClass::subroutine" : (!moore.class<@testClass>) -> ()
moore.class.methoddecl @testSubroutine -> @"testClass::testSubroutine" : (!moore.class<@testClass>) -> ()
}
moore.vtable @testClass::@vtable {
moore.vtable @realFunctionClass::@vtable {
moore.vtable @virtualFunctionClass::@vtable {
moore.vtable_entry @subroutine -> @"testClass::subroutine"
}
moore.vtable_entry @testSubroutine -> @"testClass::testSubroutine"
}
moore.vtable_entry @subroutine -> @"testClass::subroutine"
moore.vtable_entry @testSubroutine -> @"testClass::testSubroutine"
}
func.func private @"testClass::subroutine"(%arg0: !moore.class<@testClass>) {
return
}
func.func private @"testClass::testSubroutine"(%arg0: !moore.class<@testClass>) {
return
}

moore.class.classdecl @tClass extends @testClass {
moore.class.methoddecl @subroutine -> @"tClass::subroutine" : (!moore.class<@tClass>) -> ()
}
moore.vtable @tClass::@vtable {
moore.vtable @testClass::@vtable {
moore.vtable @realFunctionClass::@vtable {
moore.vtable @virtualFunctionClass::@vtable {
moore.vtable_entry @subroutine -> @"tClass::subroutine"
}
moore.vtable_entry @testSubroutine -> @"testClass::testSubroutine"
}
moore.vtable_entry @subroutine -> @"tClass::subroutine"
moore.vtable_entry @testSubroutine -> @"testClass::testSubroutine"
}
moore.vtable_entry @subroutine -> @"tClass::subroutine"
}
func.func private @"tClass::subroutine"(%arg0: !moore.class<@tClass>) {
return
}
Loading