Skip to content

Structs and Headers flattening pass#309

Open
PietroGhg wants to merge 1 commit intop4lang:mainfrom
PietroGhg:pietro/flatten_struct
Open

Structs and Headers flattening pass#309
PietroGhg wants to merge 1 commit intop4lang:mainfrom
PietroGhg:pietro/flatten_struct

Conversation

@PietroGhg
Copy link
Copy Markdown
Contributor

Adds a FlattenStructs pass that ensures that P4HIR Structs only contain Headers or scalars, and that Headers only contain scalars.
Addresses #251

@asl asl self-requested a review January 30, 2026 06:56
@PietroGhg PietroGhg force-pushed the pietro/flatten_struct branch 2 times, most recently from 6751be6 to 332f9af Compare January 30, 2026 08:15
Copy link
Copy Markdown
Collaborator

@mtsamis mtsamis left a comment

Choose a reason for hiding this comment

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

Thanks for the PR, please see some comments for potential improvements.


target.addDynamicallyLegalOp<P4HIR::StructExtractOp>([&](P4HIR::StructExtractOp op) {
return typeConverter.isLegal(op.getInput().getType());
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume there is some reason that P4HIR::StructFieldRefOp and P4HIR::StructExtractOp should be explicitly defined in addition to configureUnknownOpDynamicallyLegalByTypes? If so, maybe a comment would be good to explain.

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.

Since the conversion pattern I added merges together a sequence of struct access ops, I wanted to only declare the "root" operations of a struct access chain as illegal. But given that the conversion happens top-down, the root operation will be the first to match so we can just remove these lines of code

isPredicateOrRefToPredicate(resultTy, StructFlatteningTypeConverter::isHeader) ||
isPredicateOrRefToPredicate(resultTy, StructFlatteningTypeConverter::isScalar) ||
isPredicateOrRefToPredicate(resultTy,
[](Type ty) { return isa<P4HIR::ValidBitType>(ty); });
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think isPredicateOrRefToPredicate doesn't provide a lot of utility or readability here, maybe better to do something like below?

mlir::Type unwrapRef = leafType;
if (auto refType = dyn_cast<P4HIR::ReferenceType>(leafType))
  leafType = refType.getObjectType();

bool isLeaf = StructFlatteningTypeConverter::isHeader(leafType)
              || StructFlatteningTypeConverter::isScalar(leafType)
              || isa<P4HIR::ValidBitType>(leafType);

ConversionPatternRewriter &rewriter) const {
return llvm::TypeSwitch<Operation *, LogicalResult>(op)
.Case([&](P4HIR::StructFieldRefOp fieldRefOp) {
return processStructAccessOp(fieldRefOp, convertedInput, parentFieldPath, eraseList,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you want to avoid some duplication here you can use this pattern:

.Case<P4HIR::StructFieldRefOp, P4HIR::StructExtractOp>([&](auto op) {
  return processStructAccessOp(fieldRefOp, convertedInput, parentFieldPath, eraseList, replacements, rewriter);
}

bool outputIsRef = isa<P4HIR::ReferenceType>(op.getResult().getType());
Value newResult =
llvm::TypeSwitch<Type, Value>(convertedInput.getType())
.Case<P4HIR::ReferenceType>([&](auto) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a bit hard to read due to the identation. I would propose that TypeSwitch doesn't provide a lot of benefit since there's only one case and you could use something like

Value newResult;
if (mlir::isa<P4HIR::ReferenceType>(convertedInput.getType())) {
  ...
  newResult = ...;
} else {
  newResult = rewriter.create(...);
} 

That should eliminate 3 identation levels and make it easier to read.

// For intermediate reads we just process the rest of the tree and add the read to the erase
// list
for (auto user : op.getResult().getUsers()) {
if (!isa<P4HIR::StructFieldRefOp, P4HIR::StructExtractOp>(user)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove brackets for consistency with if below.


populateFunctionOpInterfaceTypeConversionPattern<P4HIR::FuncOp>(patterns, typeConverter);

if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove brackets 🙂

auto moduleOp = getOperation();

StructFlatteningTypeConverter typeConverter;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can remove empty line here.

class StructFlatteningTypeConverter : public P4HIRTypeConverter {
public:
StructFlatteningTypeConverter() {
addConversion([](P4HIR::StructLikeTypeInterface type) -> std::optional<Type> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

-> std::optional<Type> shouldn't really be needed here since flattenStructLikeType doesn't return one.

continue;
} else {
llvm::errs() << "Unexpected type " << fieldType << "\n";
llvm_unreachable("Unexpected field type during flattening");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

llvm::errs + llvm_unreachable feels a bit strange here. Consider that due to llvm_unreachable there (probably) won't be any message printed in a release build, if that case was ever hit.

So, either use llvm_unreachable only with that message, or keep llvm::errs() and don't use unreachable. (I would propose to only keep llvm_unreachable, if this indeed should be unreachable).

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.

Yeah the errs was left over from debugging, thanks for spotting it. I've removed it

// CHECK-SAME: %[[ARG0:.*]]: !p4hir.ref<!metadata>,
// CHECK-SAME: %[[ARG1:.*]]: !p4hir.ref<!b16i>,
// CHECK-SAME: %[[ARG2:.*]]: !p4hir.ref<!b16i>) {
// CHECK: %[[VAL_0:.*]] = p4hir.read %[[ARG0]] : <!metadata>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor thing, but I think it would be better to use CHECK-DAG here, and in the other functions matched.

Signed-off-by: Pietro Ghiglio <pghiglio@accesssoftek.com>
@PietroGhg PietroGhg force-pushed the pietro/flatten_struct branch from 332f9af to 7784f46 Compare February 3, 2026 12:58
Copy link
Copy Markdown
Collaborator

@mtsamis mtsamis left a comment

Choose a reason for hiding this comment

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

Looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants