Skip to content

Scalarize uninitialized nested POD records - #754

Closed
shankarapailoor wants to merge 13 commits into
mainfrom
shankara/pod-nondet-fix
Closed

shankarapailoor wants to merge 13 commits into
mainfrom
shankara/pod-nondet-fix

Conversation

@shankarapailoor

Copy link
Copy Markdown
Contributor

Summary

Keep unread nested POD records in explicit pod.new storage while mem2reg promotes their parent allocation. This lets later SROA and mem2reg rounds recursively scalarize the nested POD instead of leaving a residual POD-typed llzk.nondet.

Related issues

None.

Changes

  • Materialize an uninitialized POD record as pod.new in NewPodOp::getDefaultValue.
  • Add a focused PodToScalar regression for reading an uninitialized nested POD leaf.
  • Add a changelog entry for the corrected scalarization behavior.

Testing

  • nix develop --command bash -c "build/bin/llzk-opt --llzk-pod-to-scalar test/Transforms/PodToScalar/late_pod_nondet.llzk | FileCheck test/Transforms/PodToScalar/late_pod_nondet.llzk"
  • nix develop --command bash -c "cmake --build build --target check-lit" — 434 passed, 3 expected failures, 4 unsupported.
  • nix build -L — passed, including release build, lit tests, and unit/C API tests.

Submission checklist

  • If I am an external contributor, this PR has a linked issue marked approved; otherwise, this does not apply.
  • I added or updated tests for all relevant behavior, or explained above why tests are not needed.
  • I updated the relevant TableGen or other documentation, or explained above why documentation is not needed.
  • I added a changelog entry describing user-visible changes.
  • I enabled Allow edits from maintainers if this PR comes from a fork.

No TableGen documentation changes are needed; the implementation comment documents why nested POD defaults remain explicit storage.

AI assistance

  • No AI tools contributed to this PR.
  • AI tools contributed to this PR.

Tools used: OpenAI Codex.

How the tools contributed: Investigated the residual POD IR, reduced it to a focused regression, implemented the targeted fix, and drafted the changelog and PR description.

How I verified the contribution: Rebuilt llzk-opt, ran the focused FileCheck regression, ran the complete lit suite, and completed nix build -L successfully.

@shankarapailoor
shankarapailoor requested a review from a team as a code owner September 22, 2026 19:35
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

  2 files  ±0    2 suites  ±0   3m 7s ⏱️ +49s
445 tests +3  441 ✅ +3  4 💤 ±0  0 ❌ ±0 
890 runs  +6  882 ✅ +6  8 💤 ±0  0 ❌ ±0 

Results for commit 8ee913d. ± Comparison against base commit 24250ae.

♻️ This comment has been updated with latest results.

Comment thread test/Transforms/PodToScalar/late_pod_nondet.llzk Outdated
@tim-hoffman

Copy link
Copy Markdown
Member

@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: 0d0b8c59ed

ℹ️ 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/Dialect/POD/IR/Ops.cpp Outdated
// can recursively scalarize it. A POD-typed `llzk.nondet` would no longer be visible to the
// allocation-based scalarization fixpoint.
if (auto podType = llvm::dyn_cast<PodType>(slot.elemType)) {
return builder.create<NewPodOp>(getLoc(), podType);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve affine instantiations on the nested allocation

When the uninitialized nested POD contains affine-parameterized array or struct records, the outer single-record pod.new carries the required map operands, but this builder overload creates the replacement NewPodOp with empty mapOperands and numDimsPerMap. NewPodOp::verifyAffineMapOperands requires one instantiation group per recursively nested affine map, so llzk-pod-to-scalar produces invalid IR and fails verification for these valid inputs instead of scalarizing them; propagate the original allocation's map operand groups and dimension counts to the nested allocation.

Useful? React with 👍 / 👎.

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.

Fixed in 1cb6895. The nested default now forwards the outer single-record allocation affine-map operand groups and dimension counts. Added an affine nested-POD regression to late_pod_nondet.llzk.

Comment thread lib/Dialect/POD/IR/Ops.cpp Outdated
// can recursively scalarize it. A POD-typed `llzk.nondet` would no longer be visible to the
// allocation-based scalarization fixpoint.
if (auto podType = llvm::dyn_cast<PodType>(slot.elemType)) {
return builder.create<NewPodOp>(getLoc(), podType);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve copy semantics for repeated nested-record reads

When an uninitialized nested record is read more than once and one returned copy is mutated, mem2reg replaces every pre-store read of the outer slot with this same NewPodOp value. POD reads are documented as value copies rather than aliases, so code that reads %outer[@inner] twice, writes a leaf through the first result, and then reads that leaf through the second must still observe the original nondeterministic value; after this change, later SROA/mem2reg rounds see one shared allocation and incorrectly forward the write to the second copy. Materialize independent snapshots for POD-valued reads or reject this mutable-copy case rather than using one allocation as the shared default.

Useful? React with 👍 / 👎.

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.

Fixed in 1cb6895. getPromotableSlots now declines mem2reg promotion when a POD-valued read is mutated, preserving independent value-copy semantics. Added a focused two-read and one-write mem2reg regression.

Comment thread lib/Dialect/POD/IR/Ops.cpp Outdated
// can recursively scalarize it. A POD-typed `llzk.nondet` would no longer be visible to the
// allocation-based scalarization fixpoint.
if (auto podType = llvm::dyn_cast<PodType>(slot.elemType)) {
return builder.create<NewPodOp>(getLoc(), podType);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Erase an unused nested default after promotion

When --mem2reg promotes an outer POD whose nested-record read has no live users, this newly created default becomes unused, but handlePromotionComplete only erases unused defaults whose defining op is llzk.nondet. The original outer allocation is erased while this effectful pod.new remains, so a standalone mem2reg invocation now leaves a dead allocation that the previous nondeterministic-default path explicitly removed; include NewPodOp in the unused-default cleanup (the PodToScalar pipeline's later cleanup masks this only for that pipeline).

Useful? React with 👍 / 👎.

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.

Fixed in 1cb6895. Promotion cleanup now erases an unused nested pod.new default as well as llzk.nondet. Added a standalone mem2reg regression verifying no dead allocation remains.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T15:45:57.291265Z 8ee913d Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 1d925a1d8a

ℹ️ 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 on lines +16 to +25
// CHECK-LABEL: module attributes {llzk.lang} {
// CHECK-NEXT: function.def @preserve_mutated_read_copy(%arg0: index) -> index {
// CHECK-NEXT: %pod = pod.new : <[@inner: !pod.type<[@value: index]>]>
// CHECK-NEXT: %0 = pod.read %pod[@inner] : <[@inner: !pod.type<[@value: index]>]>, !pod.type<[@value: index]>
// CHECK-NEXT: %1 = pod.read %pod[@inner] : <[@inner: !pod.type<[@value: index]>]>, !pod.type<[@value: index]>
// CHECK-NEXT: pod.write %0[@value] = %arg0 : <[@value: index]>, index
// CHECK-NEXT: %2 = pod.read %1[@value] : <[@value: index]>, index
// CHECK-NEXT: function.return %2 : index
// CHECK-NEXT: }
// CHECK-NEXT: }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use lit variables as generated by the python script

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.

Fixed in fc8850c. The nested POD checks now use the FileCheck variables emitted by scripts/generate-test-checks.py while keeping the checks minimized and omitting the autogenerated disclaimer. The focused regression and full check-lit suite pass.

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

Addressed the latest external review in 0a468ed:

  • Nested default pod.new creation now uses an insertion guard and is placed immediately before the original allocation, so block-local affine map operands dominate it.
  • The transitive read-only proof now follows array-valued POD reads and array.extract results through ArrayAccessOpInterface; array writes/inserts, forwarding, escapes, and unrecognized aggregates reject promotion.
  • Added regressions for a block-local arith.constant affine operand and mutation through a copied extracted subarray.
  • Added the map-order identity rationale for the single-record case.

I did not broaden the promotion gate to every flat aggregate record in this PR; that is the larger pre-existing case. I also checked the suggested plain --mem2reg positive cascade: it advances one nested level rather than fully scalarizing the chain in one run, while the PodToScalar driver supplies the repeated rounds, so I did not add a test asserting otherwise.

Focused checks, full check-lit, and nix build -L pass.

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 0a468ed00a

ℹ️ 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".

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

@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: 52d4836b69

ℹ️ 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 on lines +234 to +238
} else if (auto writeOp = llvm::dyn_cast<WritePodOp>(user)) {
if (writeOp.getPodRef() == aggregateValue) {
if (!user->isBeforeInBlock(ownedRead)) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject untransferred mutations of earlier snapshots

When an earlier snapshot is mutated before ownedRead but that changed record is not subsequently transferred into ownedValue, this branch still declares promotion safe. For example, snapshot = read outer; write snapshot.value = replacement; owner = read outer; write outer.inner = owner; read owner.value must return the original nondeterministic value because POD reads are copies, but promotion aliases both reads to the same nested pod.new, so the later scalarization returns replacement. Only permit a mutation of an earlier snapshot when the analysis proves the mutated aggregate is copied into the owner before writeback; otherwise decline promotion.

Useful? React with 👍 / 👎.

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.

Fixed in 82f208b. Snapshot-use analysis now rejects a directly mutated aggregate unless that aggregate is explicitly transferred into the owned value before the first writeback. I added a regression for the untransferred earlier-snapshot case while retaining the positive Circom-style array mutation/transfer coverage. Focused FileCheck tests and the full nix build -L (including all 445 lit tests and unit tests) pass.

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

@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: 8ee913d6ad

ℹ️ 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 on lines +324 to +327
return llvm::all_of(reads, [ownedRead, ownedValue, firstWriteback](ReadPodOp readOp) {
return readOp == ownedRead ||
hasOnlySnapshotUses(readOp.getResult(), ownedRead, ownedValue, firstWriteback);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject promotions that alias interacting snapshots

When there are multiple snapshots before ownedRead, this checks each snapshot independently and misses interactions between them. If snapshot A mutates a nested leaf and later transfers that leaf into ownedValue, while snapshot B reads the same leaf after A's mutation but before ownedRead, both snapshots pass this predicate; mem2reg then replaces A and B with the same default NewPodOp, so B observes A's mutation even though pod.read returns an independent value copy. Reject such promotion whenever another snapshot is observed after an accepted snapshot mutation, or model the cross-snapshot ordering explicitly.

Useful? React with 👍 / 👎.

Comment on lines +382 to +384
return builder.create<NewPodOp>(
getLoc(), podType, mapOperands, getNumDimsPerMapAttr(), InitializedRecords {}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep affine nested defaults destructurable

For an uninitialized outer record whose nested POD has multiple records and contains an affine-sized array, this creates a multi-record pod.new with nonempty map operands. NewPodOp::getDestructurableSlots() rejects every such allocation when getMapOperands() is nonempty, while mem2reg rejects it because it has more than one record, so the scalarization loop reaches a fixed point with residual POD IR and --llzk-pod-to-scalar fails on otherwise valid input. The affine operand groups need to remain available while allowing this nested allocation to be split record-by-record.

Useful? React with 👍 / 👎.

@shankarapailoor

Copy link
Copy Markdown
Contributor Author

I'm going to close this PR as I think pod-to-scalar needs a more fundamental refactor because this stuff is getting too complicated.

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