Skip to content

Fuse matching product scf.if regions - #484

Open
1sgtpepper wants to merge 1 commit into
project-llzk:mainfrom
1sgtpepper:fuse-product-if-regions
Open

Fuse matching product scf.if regions#484
1sgtpepper wants to merge 1 commit into
project-llzk:mainfrom
1sgtpepper:fuse-product-if-regions

Conversation

@1sgtpepper

@1sgtpepper 1sgtpepper commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Partially addresses #300.

Adds conservative scf.if fusion to -llzk-fuse-product-loops for matching compute/constrain product regions.

The fusion handles narrow cases where matching compute and constrain scf.if regions have the same SSA condition and can be fused without changing member read/write ordering. It rejects constrain-side member reads, storage writes, calls, witness generation, and write-like memory effects, so member reads remain tied to their original storage semantics until a more precise remapping analysis is added.

Product control-flow fusion remains scoped to explicitly marked product-source control ops.

Testing

  • git diff --check
  • Added focused lit/FileCheck coverage under test/Transforms/FuseProductLoops/ for:
    • fusable scf.if cases, including direct compute-result uses and multi-result remapping
    • condition mismatch, member-read, read-after-write, and call-after-write no-fuse cases
    • moved constrain-branch rejection for array writes/inserts, witness calls, member writes, global writes, POD writes, and RAM stores
    • untagged outer scf.if/scf.for product-source inference no-fuse cases

@iangneal
iangneal requested a review from a team May 28, 2026 01:26
@iangneal

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4213c67d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/Transforms/LLZKFuseProductLoopsPass.cpp Outdated

@raghav198 raghav198 left a comment

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.

The constrain use is remapped to the branch-local compute value instead of hoisting the read.

I may be misunderstanding, but I think this is not safe when the struct.readm is a constrain op:

%0 = felt.add %a, %b {source = "compute"}
struct.writem %self[@foo] = %0 {source = "compute"}
%foo = struct.readm %self[@foo] {source = "constrain"}
constrain.eq %foo, %c {source = "constrain"}

emits a constraint guaranteeing that the @foo signal equals the result of %c, whereas in the remapped

%0 = felt.add %a, %b {source = "compute"}
struct.writem %self[@foo] = %0 {source = "compute"}
constrain.eq %0, %c {source = "constrain"}

the emitted constraint doesn't refer to a struct signal at all.

@1sgtpepper

Copy link
Copy Markdown
Contributor Author

Thanks for catching this, Raghav. You're right. Remapping the struct.readm result to the branch local compute value can remove the member signal from the emitted constraint. I'll make this fusion case conservative for now and add a regression test so we keep the constraint tied to the member read.

Copilot AI left a comment

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.

Pull request overview

This PR extends the -llzk-fuse-product-loops pass to also fuse matching compute/constrain scf.if regions in struct product programs (Fixes #300), and adds regression tests covering fusable and non-fusable scf.if patterns.

Changes:

  • Add scf.if fusion logic that clones compute + constrain branches into a single fused scf.if when conditions and structural constraints match.
  • Refactor pass entry/recursion to fuse both scf.if and scf.for control flow via a shared fuseMatchingRegionControlFlow routine.
  • Add lit/FileCheck tests for scf.if fusion and “do not fuse” guardrails; add an unreleased changelog entry.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/Transforms/LLZKFuseProductLoopsPass.cpp Implements scf.if fusion and refactors recursion over region control flow.
test/Transforms/FuseProductLoops/fuse_if_no_member_read.llzk Positive test ensuring scf.if fusion occurs in a safe case.
test/Transforms/FuseProductLoops/fuse_loop_then_if.llzk Ensures loop fusion still works and enables nested scf.if fusion after loop fusion.
test/Transforms/FuseProductLoops/no_fuse_if_call_after_write.llzk Negative test: blocks fusion when a call occurs after a mapped write.
test/Transforms/FuseProductLoops/no_fuse_if_condition_mismatch.llzk Negative test: blocks fusion when conditions differ.
test/Transforms/FuseProductLoops/no_fuse_if_internal_read.llzk Negative test: blocks fusion when constrain side reads members inside the scf.if.
test/Transforms/FuseProductLoops/no_fuse_if_read_after_write.llzk Negative test: blocks fusion when a member read appears between the ifs.
changelogs/unreleased/fuse-product-if-regions.yaml Changelog entry for the new scf.if fusion capability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/Transforms/LLZKFuseProductLoopsPass.cpp Outdated
Comment thread lib/Transforms/LLZKFuseProductLoopsPass.cpp
@iangneal

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 326acc24af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@1sgtpepper

Copy link
Copy Markdown
Contributor Author

I seem to have found some loose ends, will update in a bit.

@1sgtpepper

Copy link
Copy Markdown
Contributor Author

Pushed a conservative safety guard for moved constrain branches.

Comment thread CHANGELOG.md Outdated
@iangneal

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 8725011bd2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread include/llzk/Transforms/LLZKTransformationPasses.td
Comment on lines +174 to +175
return effectOp.hasEffect<MemoryEffects::Write>() ||
effectOp.hasEffect<MemoryEffects::Allocate>() || effectOp.hasEffect<MemoryEffects::Free>();

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.

I believe you can either do:
effectOp.hasEffect<MemoryEffects::Write, MemoryEffects::Allocate, MemoeryEffects::Free>(); or at least hasEffect<MemoryEffects::Write, MemoryEffects::Allocate, MemoeryEffects::Free>(effectOp);

Comment on lines +133 to +143
static std::optional<llvm::StringRef> getProductSource(Operation *op) {
if (StringAttr source = op->getAttrOfType<StringAttr>(PRODUCT_SOURCE)) {
return source.getValue();
}
return std::nullopt;
}

static inline bool hasProductSource(Operation *op, llvm::StringRef source) {
std::optional<llvm::StringRef> productSource = getProductSource(op);
return productSource && *productSource == source;
}

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.

I think we should move these functions to a shared helper file so they can be reused, e.g. by llzk-compute-constrain-to-product-pass

effectOp.hasEffect<MemoryEffects::Allocate>() || effectOp.hasEffect<MemoryEffects::Free>();
}

static bool hasUnsafeMovedConstrainOp(scf::IfOp constrainIf) {

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.

Add documentation to functions, especially here, since it's not clear what you're trying to do.

Comment on lines +184 to +195
if (isa<MemberReadOp, MemberWriteOp, llzk::global::GlobalWriteOp, llzk::array::WriteArrayOp,
llzk::array::InsertArrayOp, llzk::pod::WritePodOp, llzk::ram::StoreOp>(op)) {
return WalkResult::interrupt();
}

if (op->hasTrait<llzk::function::WitnessGen>() || hasWriteLikeMemoryEffect(op)) {
return WalkResult::interrupt();
}

if (isa<CallOpInterface>(op)) {
return WalkResult::interrupt();
}

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 could all be one if statement.

) {
// `scf.if` fusion moves the constrain branch before intervening member writes of compute-if
// results. The moved branch must not read members, write storage, call functions, or have
// write-like effects.

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.

Currently this loop only checks member reads/writes, but ignores RAM/globals

// CHECK: struct.writem %{{[0-9a-zA-Z_\.]+}}[@foo] = %[[RESULT]] : <@A>, !felt.type
// CHECK: %[[FOO:[0-9a-zA-Z_\.]+]] = struct.readm %{{[0-9a-zA-Z_\.]+}}[@foo] : <@A>, !felt.type
// CHECK: scf.if %[[B]] {
// CHECK: constrain.eq %[[FOO]]

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.

Lit test output should show the whole module, like all other lit tests in this repo.

@1sgtpepper
1sgtpepper force-pushed the fuse-product-if-regions branch from 53ba923 to 6719d38 Compare July 16, 2026 14:11
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.

5 participants