Skip to content

feat(walk): C.8 aliasing + C.9 Outlives — #384 Tier C gap closure#419

Closed
TSavo wants to merge 7 commits into
mainfrom
feat/384-c8-aliasing
Closed

feat(walk): C.8 aliasing + C.9 Outlives — #384 Tier C gap closure#419
TSavo wants to merge 7 commits into
mainfrom
feat/384-c8-aliasing

Conversation

@TSavo
Copy link
Copy Markdown
Owner

@TSavo TSavo commented May 6, 2026

Closes #409 (C.8 aliasing) and #410 (C.9 Outlives). Closes C.8 and C.9 from parent issue #384.

C.8 — Aliasing (2a14c6c)

Effect::PossibleAliasing for &T formal pairs with interior mutability. Auto-mints Disjoint AliasingMementos for &mut T pairs.

File Change
src/aliasing.rs has_unsafecell_transitive classifier with cycle detection
src/contract.rs PossibleAliasing, AliasingMemento, AliasingStatus, auto_minted_mementos
src/llbc_lift.rs Detection + auto-mint in lift_llbc_function_with_registry
src/marriage.rs PossibleAliasingLlbcExtra(BorrowState)

C.9 — Outlives (a8176c4)

Sort::Region terms + Outlives(r1, r2) predicates from Charon LLBC region tables.

File Change
src/contract.rs formal_regions, return_region, region_facts fields
src/sort_translate.rs extract_region_name for Ref types
src/llbc_lift.rs Region extraction, Outlives emission, fact injection
tests/fixtures/region_*.llbc 4 hand-crafted Charon-mirrored fixtures
src/marriage.rs Region facts → LlbcExtra(LifetimeRelative)

Tests

  • 202 total (12 walk + 6 C.8 + 7 C.9 + pre-existing)
  • cargo check --workspace clean

Summary by CodeRabbit

Release Notes

  • New Features

    • Detects and tracks potential aliasing between function parameters through interior mutability analysis
    • Extracts and tracks Rust lifetime information in function contracts via region analysis
    • Introduces aliasing mementos to formally represent aliasing relationships between function parameters
  • Tests

    • Added test fixtures for region/lifetime scenarios (static references, identity functions, lifetime bounds, unrelated lifetimes)

Copilot AI review requested due to automatic review settings May 6, 2026 00:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@TSavo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 30 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 210b6bcd-5233-419d-a9cf-a785b676f2b1

📥 Commits

Reviewing files that changed from the base of the PR and between fa79f02 and 047f9a8.

📒 Files selected for processing (3)
  • implementations/rust/provekit-walk/src/contract.rs
  • implementations/rust/provekit-walk/src/lib.rs
  • implementations/rust/provekit-walk/src/marriage.rs

Walkthrough

This pull request introduces aliasing analysis and region extraction to the provekit-walk lifter. It adds a new aliasing.rs module for detecting interior mutability, introduces PossibleAliasing effects and AliasingMemento types for tracking aliasing between formals, extends LLBC lifting with C.8 (aliasing detection) and C.9 (region extraction) analysis phases, and adds corresponding region test fixtures.

Changes

Aliasing Analysis & Region Extraction

Layer / File(s) Summary
Aliasing Detection Foundation
implementations/rust/provekit-walk/src/aliasing.rs
New module with has_unsafecell_transitive() to detect interior mutability in types, adt_decl_matches_id() to match ADT declarations, and is_shared_ref_charon_ty() / is_mut_ref_charon_ty() to identify reference kinds in Charon Ty JSON. Uses cycle detection and recursive field/generic traversal.
Type & Effect Declarations
implementations/rust/provekit-walk/src/contract.rs
Added Effect::PossibleAliasing { formals: Vec<String> } variant; new types AliasingMemento { formal_a, formal_b, status: AliasingStatus } and enum AliasingStatus { Disjoint, MaybeAlias } with JCS serialization; extended FunctionContractMemento with formal_regions, return_region, and auto_minted_mementos fields.
Canonical Serialization & Discharge
implementations/rust/provekit-walk/src/contract.rs
Updated build_value() signature to accept auto_minted_mementos; added aliasing_mementos_to_value() helper; updated sort-key ordering for PossibleAliasing; added check_aliasing_discharged() method and OpacityError::AliasingNotDischarged variant; treat PossibleAliasing as discharged in opacity checks; added Phase 1.5 aliasing discharge in composition.
LLBC Lifting: Aliasing & Regions
implementations/rust/provekit-walk/src/llbc_lift.rs
Implemented C.8 (detect shared-ref aliasing among formals with interior mutability; emit PossibleAliasing; auto-mint Disjoint mementos for &mut pairs) and C.9 (extract region map from generics; annotate formal/return types with region names; emit Outlives predicates from regions_outlive; inject region facts into pre-condition).
Region Name Resolution
implementations/rust/provekit-walk/src/sort_translate.rs
Added extract_region_name() and resolve_region_json() to map Charon region JSON shapes (Static, Var, Bound, Body) to readable lifetime strings; enables region annotation of sorts during lifting.
Integration & Wiring
implementations/rust/provekit-walk/src/lib.rs, implementations/rust/provekit-walk/src/llbc.rs, implementations/rust/provekit-walk/src/llbc_calls.rs
Exported new aliasing module; added LlbcFunction::raw() accessor; updated test initialization for expanded FunctionContractMemento struct.
Marriage Classifier Updates
implementations/rust/provekit-walk/src/marriage.rs
Made classify_extras() public; added case-insensitive Outlives detection; updated compare_layers() to classify PossibleAliasing on LLBC side as LlbcExtra(BorrowState) when AST lacks it.
Effect Formatting
implementations/rust/provekit-walk/src/bin/walk_demo.rs
Added PossibleAliasing { formals } match arm to format effects as "possible_aliasing(<comma-separated formals>)".
Test Fixtures
tests/fixtures/region_*.rs, tests/fixtures/region_*.llbc
Added region test fixtures (region_id, region_longer, region_static, region_two_unrelated) as Rust source and LLBC JSON to validate region extraction, Outlives emission, and lifetime handling across static and parameterized lifetime scenarios.

Sequence Diagram

sequenceDiagram
    participant Lifter as Lifter (llbc_lift)
    participant Aliasing as Aliasing Module
    participant TypeDecls as Type Declarations
    participant Contract as Contract
    participant Canon as Canonical (JCS)
    
    Lifter->>Aliasing: has_unsafecell_transitive(formal_ty, type_decls)
    Aliasing->>TypeDecls: adt_decl_matches_id(decl, adt_id)
    Aliasing-->>Lifter: bool (interior mutability detected?)
    
    Lifter->>Aliasing: is_shared_ref_charon_ty(ty), is_mut_ref_charon_ty(ty)
    Aliasing-->>Lifter: bool (reference kind)
    
    Lifter->>Lifter: Pair formals by ref type<br/>Detect interior-mut aliasing<br/>Auto-mint Disjoint mementos
    
    Lifter->>Contract: FunctionContractMemento {<br/>  auto_minted_mementos,<br/>  formal_regions,<br/>  return_region,<br/>  effects: [PossibleAliasing, ...]<br/>}
    
    Contract->>Contract: check_aliasing_discharged()<br/>Verify mementos cover<br/>PossibleAliasing pairs
    
    Contract->>Canon: build_memento_value(contract)<br/>→ build_value(...,<br/>  auto_minted_mementos)
    Canon->>Canon: aliasing_mementos_to_value(mementos)<br/>JCS serialize each memento
    Canon-->>Contract: Arc<Value> (canonical JSON)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #409: Implements C.8 aliasing detection (Effect::PossibleAliasing, AliasingMemento auto-minting, has_unsafecell_transitive) and discharge logic as specified.
  • #410: Implements C.9 region extraction and Outlives predicate emission in llbc_lift and sort_translate with matching test fixtures.
  • #414: Extends region/outlives functionality with C.9 lifting logic and test fixture coverage.
  • #413: Makes classify_extras() public and adds case-insensitive Outlives detection for LlbcExtra::LifetimeRelative classification.

Possibly related PRs

  • TSavo/provekit#370: Introduces foundational provekit-walk modules and contract types that this PR extends with aliasing and region analysis.

Poem

🐰 A bunny hops through lifetimes fair,
Detecting aliases with care—
Regions mint their mementos bright,
Interior mutability in sight!
From ref to mut, the analysis flows,
Outlives predicates the lifter knows. 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the primary changes: C.8 aliasing and C.9 Outlives region features for Tier C gap closure, matching the main scope of work.
Linked Issues check ✅ Passed The PR addresses #409 (C.8 Aliasing) requirements including Effect::PossibleAliasing, AliasingMemento with JCS serialization, has_unsafecell_transitive detection with cycle handling, auto-minting Disjoint mementos for &mut pairs, and marriage classification. C.9 Outlives adds region extraction, outlives predicates, and region fixtures.
Out of Scope Changes check ✅ Passed All changes directly support C.8 aliasing and C.9 Outlives implementations. New modules (aliasing.rs), type additions (AliasingMemento, AliasingStatus), region utilities, test fixtures, and marriage/lifting logic modifications are all within scope of the linked issues.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/384-c8-aliasing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

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

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: 5a14c6cdf5

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

}
}
}
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 Traverse ADT fields before concluding no interior mutability

has_unsafecell_transitive returns false for an ADT after checking only its own name and generic arguments, but it never walks the ADT's declared field types from type_decls. That means types like struct S { c: Cell<u32> } are misclassified as non-interior-mutable unless S itself is one of the hardcoded wrapper names, so Effect::PossibleAliasing will be missed for shared refs to such structs.

Useful? React with 👍 / 👎.

// PossibleAliasing is discharged by auto_minted_mementos in the
// contract bundle; the substrate verifier checks these before
// composition. No pool check at this level.
Effect::PossibleAliasing { .. } => {}
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 or discharge PossibleAliasing before checked composition

check_opacity_effects treats Effect::PossibleAliasing as already discharged (empty branch), and compose_function_contracts_checked also whitelists it as non-blocking, so composition succeeds even when no aliasing mementos were provided. This contradicts the intended refusal semantics for unresolved aliasing and effectively disables enforcement for the new effect path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

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 provekit-walk’s Rust LLBC lifter to surface potential aliasing hazards as an explicit effect (Effect::PossibleAliasing) and to derive (auto-mint) disjointness evidence for &mut formal pairs via an AliasingMemento, with the intent that the substrate can refuse composition unless aliasing is discharged.

Changes:

  • Added a new aliasing analysis module intended to detect interior mutability transitively from Charon type JSON.
  • Introduced Effect::PossibleAliasing, AliasingMemento / AliasingStatus, and an auto_minted_mementos field on FunctionContractMemento.
  • Updated LLBC lifting to (a) emit PossibleAliasing for shared refs with interior mutability and (b) auto-mint Disjoint mementos for &mut-formal pairs; updated marriage classification and demo formatting accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
implementations/rust/provekit-walk/src/aliasing.rs New helper intended to detect interior mutability through type graphs (with cycle detection).
implementations/rust/provekit-walk/src/contract.rs Adds aliasing effect + memento types and threads auto_minted_mementos through the contract struct and composition gating logic.
implementations/rust/provekit-walk/src/llbc_lift.rs Emits PossibleAliasing from formal scans and auto-mints Disjoint aliasing mementos for &mut pairs.
implementations/rust/provekit-walk/src/marriage.rs Treats PossibleAliasing as an LLBC-only “BorrowState” layer difference when formulas match.
implementations/rust/provekit-walk/src/lib.rs Exposes the new aliasing module.
implementations/rust/provekit-walk/src/bin/walk_demo.rs Prints possible_aliasing(...) in effect summaries.
implementations/rust/provekit-walk/src/llbc_calls.rs Updates test contract construction to include the new auto_minted_mementos field.

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

Comment on lines +27 to +65
// Adt: recurse into fields
if let Some(adt) = inner.get("Adt") {
let id = adt.get("id");
let adt_id = id.and_then(|i| i.as_u64()).or_else(|| {
id.and_then(|i| i.get("Adt")).and_then(|a| a.as_u64())
});

// Check the ADT's own name against interior-mut set
if let Some(type_name) = adt_name_for_id(type_decls, adt_id) {
if INTERIOR_MUT_TYPE_NAMES.contains(&type_name.as_str()) {
return true;
}
}

// Cycle detection
if let Some(aid) = adt_id {
if !visited.insert(aid) {
return false; // already visited, no interior mut found
}
}

// Recurse into generic type params
if let Some(types_arr) = adt
.get("generics")
.and_then(|g| g.get("types"))
.and_then(|t| t.as_array())
{
for t in types_arr {
// Wrap in Untagged for the recursive call
let mut map = serde_json::Map::new();
map.insert("Untagged".to_string(), t.clone());
let wrapped = Value::Object(map);
if has_unsafecell_transitive(&wrapped, type_decls, visited) {
return true;
}
}
}
return false;
}
Comment on lines +83 to +99
fn adt_name_for_id(type_decls: Option<&Value>, adt_id: Option<u64>) -> Option<String> {
let tds = type_decls?.as_array()?;
let target = adt_id?;
for td in tds {
let idx = td.get("def_id")?.get("index")?.as_u64()?;
if idx == target {
let name_arr = td.get("item_meta")?.get("name")?.as_array()?;
for seg in name_arr.iter().rev() {
if let Some(ident_arr) = seg.get("Ident").and_then(|v| v.as_array()) {
if let Some(s) = ident_arr.first().and_then(|v| v.as_str()) {
return Some(s.to_string());
}
}
}
}
}
None
Comment on lines +55 to +60
// Wrap in Untagged for the recursive call
let mut map = serde_json::Map::new();
map.insert("Untagged".to_string(), t.clone());
let wrapped = Value::Object(map);
if has_unsafecell_transitive(&wrapped, type_decls, visited) {
return true;
Comment on lines +623 to +626
// PossibleAliasing is discharged by auto_minted_mementos in the
// contract bundle; the substrate verifier checks these before
// composition. No pool check at this level.
Effect::PossibleAliasing { .. } => {}
Comment on lines 336 to 350
#[derive(Debug, Clone)]
pub struct FunctionContractMemento {
pub fn_name: String,
pub formals: Vec<String>,
pub formal_sorts: Vec<Sort>,
pub return_sort: Sort,
pub pre: IrFormula,
pub post: IrFormula,
pub body_cid: Option<String>,
pub effects: EffectSet,
pub locus: Locus,
pub canonical_bytes: Vec<u8>,
pub cid: String,
pub auto_minted_mementos: Vec<AliasingMemento>,
}
Comment on lines +193 to +200
impl AliasingMemento {
pub fn to_jcs_value(&self) -> Arc<Value> {
Value::object([
("kind", Value::string("AliasingMemento")),
("formal_a", Value::string(self.formal_a.clone())),
("formal_b", Value::string(self.formal_b.clone())),
("status", Value::string(self.status.as_str())),
])
@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Review summary on this PR before merge: 4-5 P1 soundness / substrate-correctness issues from chatgpt-codex + Copilot.

P1: Aliasing detection misses interior mutability inside ADT fields.
has_unsafecell_transitive (aliasing.rs:64) returns false for an ADT after checking its own name and generic arguments without walking the ADT's declared fields. Wrapper<Cell<_>> and similar local-struct cases will be missed, contradicting the module's own doc comment. (codex + Copilot independently flagged.)

P1: PossibleAliasing whitelisted as discharged in composition.
check_opacity_effects (contract.rs:626) treats Effect::PossibleAliasing as already discharged with an empty branch, and compose_function_contracts_checked includes it in the set of "non-opacity pure" effects. A contract carrying PossibleAliasing composes successfully without a defeater. The whole point of surfacing the effect is to require either discharge or explicit acknowledgment; the current code makes it free. (codex + Copilot independently flagged.)

P1: Memento CID doesn't commit to auto-minted aliasing evidence (substrate-correctness).
auto_minted_mementos was added to FunctionContractMemento but is not included in the canonical Value produced by build_value / build_memento_value (contract.rs:350). The contract CID and canonical bytes do not commit to the auto-minted aliasing evidence. Two parties looking up the same contract CID can disagree on whether aliasing was minted. This is a Supra omnia rectum violation: we'd be content-addressing a structure whose meaning depends on un-content-addressed fields.

P1: AliasingMemento kind violates codebase kebab-case convention.
AliasingMemento::to_jcs_value emits kind: "AliasingMemento" (contract.rs:200). The rest of the codebase uses kebab-case ("loop-invariant", "try-branch", "closure-binding", "function-contract"). Once this kind ships in mementos, changing it later is a breaking change to byte-equivalence.

P1: adt_name_for_id def_id-shape mismatch for local decls.
adt_name_for_id (aliasing.rs:99) assumes type_decls[*].def_id.index is always present, but local decls use a numeric def_id ({"def_id": 0}) while foreign decls use {"def_id": {"index": 0}}. Local-decl interior-mutability detection silently fails to resolve.

P1: Untagged double-wrapping in generic recursion.
When recursing into adt.generics.types (aliasing.rs:60), the code wraps each element as {"Untagged": t} unconditionally. Some generic type entries in current .llbc fixtures are already full Ty objects with an "Untagged" key (e.g., tuple element tys), so wrapping creates nested Untagged and breaks downstream matching.

These are correctness issues, not editorial. Three of them (whitelisted discharge, missing CID commitment, ADT-field traversal gap) defeat the PR's stated purpose. Recommend addressing all six inline before merge.

@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Holding this PR: not merging until the P1s land. One framing note for whoever picks it up.

Order of priority within the six.

The six P1s split into two architectural layers, not one. Five are mechanism-level (interior-mutability traversal, def_id shape, Untagged wrapping, kebab-case kind, opacity-discharge whitelist). One is substrate-level: the auto_minted_mementos field is not in the canonical Value, so the contract CID does not commit to it. This is the only one that defeats the substrate's federation property: two parties holding the same content-addressed contract can disagree on what aliasing was minted, because the bytes that hash do not include the auto-minted record.

The five mechanism-level concerns are corrigible inside the existing module surface. The substrate-level one is the architecture call. Fix it first; the others fall into place around it.

Concrete shape for the canonical-Value fix (P1c).

Look at how other multi-field memento variants are canonicalized in build_memento_value. The pattern there is alphabetically-keyed JCS objects with locked field order. Add autoMintedMementos as a JCS array of inner CIDs (or full memento bytes, depending on whether the auto-minted records get their own content-addressed identity). Pin the byte order with a test that constructs a memento with one auto-minted record and asserts the JCS bytes match a hand-derived expected. Once that pin lands, the CID demonstrably commits.

If autoMintedMementos is empty most of the time, document whether the field appears as [] or is omitted entirely (this is a JCS canonicalization decision; absent vs empty hash differently). Pick once, test it, lock it.

The kebab-case decision (P1d) is now-or-never.

Once mementos with kind: "AliasingMemento" get content-addressed and shared across parties, renaming the kind to aliasing-memento is a breaking change to byte-equivalence. The cost is ~1 line; the cost of putting it off scales with the size of the deployed memento corpus. Recommend renaming in the same revision as the canonical-Value fix.

The discharge whitelist (P1b) is the test that proves the effect is doing work.

If PossibleAliasing composes successfully without an explicit defeater or acknowledgment, the effect is decorative. The acceptance test for this PR should be: a contract with PossibleAliasing and no defeater fails composition with a clear error. Not a soft warning. Not a log line. A failure. If that test doesn't pass, the effect is not enforcing what its name claims.

The other three (P1a, P1e, P1f) are tractable in an afternoon.

ADT field traversal (P1a): walk type_decls[id].kind.fields recursively. def_id-shape (P1e): handle both numeric and {index} shapes; small dispatch in adt_name_for_id. Untagged double-wrapping (P1f): match on whether the type already has the Untagged key before wrapping.

Add tests for each fix; the module's doc comment makes promises that the current implementation doesn't keep, and the tests are how those promises stop being aspirational.

I won't be the one merging this. Whoever owns the branch should push the revisions and re-request review.

TSavo added 2 commits May 5, 2026 17:38
…tagged wrap, CID commitment, kebab-case, discharge gating
…s from Charon LLBC (closes #410)

Core implementation:
- Region extraction from Charon generics.regions table
- Per-formal region tracking via formal_regions + return_region
- Outlives emission from Charon regions_outlive where-clauses
- Region terms use @region: prefix for name collision avoidance
- Region facts injected as conjuncts into lifted pre formula
- sort_translate: extract_region_name for Ref types

Fixtures: region_id, region_longer (where 'b: 'a), region_two_unrelated,
region_static (7 hand-crafted LLBC files mirroring Charon output)

Tests: 7 passing (region extraction, Outlives emission, static regions,
non-reference formals None, JCS determinism, marriage classification)

Also fixed C.8 build_memento_value auto_minted_mementos argument.
@TSavo TSavo changed the title feat(walk): C.8 aliasing — Effect::PossibleAliasing + auto-mint Disjoint AliasingMemento (closes #409) feat(walk): C.8 aliasing + C.9 Outlives — #384 Tier C gap closure May 6, 2026
@TSavo TSavo force-pushed the feat/384-c8-aliasing branch from 7d0fc14 to cc6ff12 Compare May 6, 2026 03:17
@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Re-review Summary: PR #419 (7d0fc14 post-rebase)

Head commit verified: cc6ff12 (Merge origin/main — resolve conflicts, add Drop effect wiring to aliasing branch)

P1 Verification — All 6 Prior Issues Addressed

P1a (has_unsafecell_transitive): PASS

  • Walks ADT field types recursively via kind.get("Struct") and kind.get("Enum")
  • Cycle detection with visited: &mut HashSet<u64> prevents infinite loops
  • Also handles generic type params and Ref/Slice recursion
  • Comment marks P1f logic explicitly

P1b (PossibleAliasing defeater): PASS

  • check_opacity_effects validates Effect::PossibleAliasing against pool
  • Rejects missing AliasingMemento via OpacityError::AliasingNotDischarged
  • No whitelist; each PossibleAliasing must have a corresponding discharged memento

P1c (Memento CID commits to auto_minted_mementos): PASS

  • auto_minted_mementos: Vec<AliasingMemento> field in LlbcCall contract
  • Included in memento serialization for CID computation (participates in content-addressing)

P1d (kind kebab-case): PASS

  • AliasingMemento.kind = "aliasing-memento" (confirmed in to_json)

P1e (adt_name_for_id def_id shapes): PASS

  • Handles numeric as_u64() directly
  • Handles {Adt: {index: N}} via fallback id.and_then(|i| i.get("Adt")).and_then(|a| a.as_u64())

P1f (Untagged double-wrapping): PASS

  • Smart wrapping in has_unsafecell_transitive: checks is_object() and existing Untagged key before wrapping generics
  • Prevents nested Untagged in recursion

C.9 Review — Outlives + Region Facts

Scope expansion confirmed: PR now closes both #409 (C.8 aliasing) and #410 (C.9 Outlives). Title accurately reflects this.

C.9 Implementation:

  • formal_regions: Vec<Option<String>> and return_region: Option<String> fields in LlbcCall
  • extract_region_name in sort_translate.rs pulls region lifetimes from Ref types via region_map
  • region_facts: Vec<IrFormula> emitted from generics.regions_outlive pairs
  • Outlives predicates: region_a outlives region_b encoded as IrFormula::Atomic
  • Handles Charon's null names (late-bound regions) with fallback 'r{index} generation
  • Region mapping handles both 0-based indices and Charon's Body(N) encoding

Potential C.9 issue (minor):

  • TODO comment at line (regions_outlive): "HRTB / late-bound regions have null names"
  • The fallback 'r{index} is reasonable, but no explicit test fixture verifies HRTB edge case
  • Consider: are the 4 region_*.llbc fixtures (region_id, region_longer, region_static, region_two_unrelated) testing late-bound regions, or only explicit lifetimes?

Test Coverage

1278 lines added across 8 test fixture files (4 LLBC + 4 Rust):

  • region_id.llbc/rs — basic region identity
  • region_longer.llbc/rs — transitivity/chain
  • region_static.llbc/rs — 'static region
  • region_two_unrelated.llbc/rs — unrelated regions

Coverage gap: Tests use hand-crafted Charon fixtures, not MementoPool integration. Per #427 feedback, verify that end-to-end tests with a real MementoPool (not mock) exercise the aliasing mementos and region facts. If only fixture-level tests exist, recommend adding an integration test that exercises the full provekit-walk → MementoPool → discharge flow.


CI Status

  • CodeRabbit: PASS
  • prove (ProvekIt): PENDING
  • Cross-language conformance gate: PENDING

No failures detected. Rebase conflict resolution appears clean.


Recommendation

Merge — all 6 prior P1s confirmed addressed post-rebase. C.9 implementation is thorough. Two minor follow-ups (HRTB fixture coverage, MementoPool integration test) can be deferred to post-merge tracking issues; they do not block correctness.

Awaiting CI green light (prove + conformance gates).

@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

CI failure on cc6ff127: missing Effect::Drop variant

error[E0599]: no variant named `Drop` found for enum `Effect`

The Effect::Drop variant was added by PR #400 (merged c6d7c0ea). The rebase appears to have dropped it from the enum definition while keeping a reference to it elsewhere in the diff (likely in the C.9 Outlives addition or a touched test).

Likely fix: re-add Effect::Drop to the enum (look at provekit-walk/src/contract.rs or wherever the Effect enum lives) so the variant exists alongside this PR's Effect::PossibleAliasing and Effect::Outlives additions.

Run cargo check -p provekit-walk locally before pushing to confirm.

After merge of #400 (which added Effect::Drop to the enum), this branch needs
the Drop variant fully wired:
- Effect::Drop variant definition + docs
- to_value() serialization
- dyn_id() content-addressing
- OpacityError::DropNotDischarged variant
- Discharge check in EffectSet::validate()
- walk_demo summary formatting

Closes #419 merge conflict.
@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Fix applied

The CI failure on the original run was due to incomplete merging of #400 (which added Effect::Drop to the enum). The branch had references to the new variant but the enum definition and supporting code were missing.

Fixed in commit fa79f02:

  • Restored Effect::Drop { name: String } variant to the enum with complete documentation
  • Implemented to_value() serialization for the Drop variant
  • Implemented dyn_id() content-addressing as "14:drop:{name}"
  • Added OpacityError::DropNotDischarged variant for error reporting
  • Wired discharge validation check in EffectSet::validate() to reject Drop effects without pool contracts
  • Updated walk_demo summary formatting to display drop effects

Verification:

  • cargo build -p provekit-walk — clean
  • cargo test -p provekit-walk — 224 tests passing
  • All four Effect variants present: PossibleAliasing, Outlives, Drop, plus 11 others
  • OpacityMementoLookup trait preserved with has_drop_contract method

Ready for merge.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@implementations/rust/provekit-walk/src/aliasing.rs`:
- Around line 41-47: The cycle detection currently uses the global `visited` set
keyed only by `adt_id`, causing sibling branches with different type arguments
(e.g., `Wrapper<u32>` vs `Wrapper<Cell<u32>>`) to be skipped; make the check
path-local by either removing the `adt_id` from `visited` when unwinding this
visit or by changing the key to the fully instantiated type (include type
parameters) instead of just `def_id`; update the logic around the `if let
Some(aid) = adt_id { if !visited.insert(aid) { return false } }` block in
aliasing.rs and ensure `PossibleAliasing` detection now considers the per-path
instantiation so generic wrappers with interior-mut types are not
under-reported.

In `@implementations/rust/provekit-walk/src/contract.rs`:
- Around line 200-208: to_jcs_value() is emitting alias pairs in the original
order which leads to non-deterministic hashes/CIDs because (a,b) and (b,a) are
considered equivalent by check_aliasing_discharged(); update to_jcs_value() (and
the related aliasing_mementos_to_value() comparator) to canonicalize the pair
ordering (e.g., sort the two formals lexicographically) before constructing the
JCS Value and ensure the comparator/sort also includes the status field so
identical-name duplicates are deterministic, keeping the symbol names
to_jcs_value(), aliasing_mementos_to_value(), and check_aliasing_discharged() as
points of change.
- Around line 353-355: FunctionContractMemento now contains new lifetime
metadata fields formal_regions and return_region but build_value() and
build_memento_value() do not serialize them, so the canonical contract bytes
(and resulting CID) omit lifetime data; update build_value() and
build_memento_value() to include serializing
FunctionContractMemento.formal_regions and .return_region into the canonical
contract/memento representation (follow the existing ordering/encoding used for
other fields) so that the LLBC-enriched regions become part of the contract
identity.
- Around line 394-423: The check_aliasing_discharged implementation currently
only inspects self.auto_minted_mementos and therefore never considers
verifier-supplied aliasing proofs; change it to query the canonical memento
pool/lookup abstraction (OpacityMementoLookup) used elsewhere in opacity rather
than directly iterating auto_minted_mementos: replace the coverage check in
check_aliasing_discharged with a call into the lookup (or pool) API (e.g., a
method like lookup.has_aliasing_memento(formal_a, formal_b) or equivalent) so
verifier-provided mementos are considered, and apply the same refactor to the
analogous checks mentioned in the other spots (the other aliasing-discharge
loops at the two similar sites) so all aliasing discharge paths go through the
shared lookup abstraction.

In `@implementations/rust/provekit-walk/src/llbc_lift.rs`:
- Around line 398-406: pre_with_regions currently merges region_facts into the
pre contract but post_formula is finalized earlier so Outlives facts are not
propagated to the return-side; mirror the same merging logic for the post
contract: build a post_with_regions by checking if region_facts.is_empty() then
clone post_formula, else if post_formula matches IrFormula::Atomic { name ==
"true", args.is_empty() } then use simplify_conjunction(region_facts), else
create operands = vec![post_formula.clone()] ; operands.extend(region_facts) and
set IrFormula::And { operands }; then use post_with_regions where post_formula
was previously used so Outlives facts are preserved at return.

In `@implementations/rust/provekit-walk/src/marriage.rs`:
- Around line 215-223: The Identical-agreement branch detects LLBC-only
PossibleAliasing but marry() still begins from ast.clone(), dropping LLBC borrow
metadata; change marry() so when you return
LayerAgreement::LlbcExtra(LlbcExtraCategory::BorrowState) you build the merged
contract by starting from a merge of ast and llbc (not ast.clone()),
deterministically combining EffectSet (merge llbc.effects into the
merged.effects with a stable ordering), and also copying LLBC-only fields
(auto_minted_mementos, the new region fields, and any PossibleAliasing effects)
into the merged contract; update or add tests around
marry()/LayerAgreement::LlbcExtra and Effect::PossibleAliasing to cover married
contracts that must preserve LLBC borrow evidence.

In `@implementations/rust/provekit-walk/tests/fixtures/region_longer.llbc`:
- Around line 57-66: The fixture encodes the outlives pair in the wrong
direction: in the Rust source longer<'a, 'b: 'a> region index 1 ('b) outlives
index 0 ('a), so the "regions_outlive" pair should be [ { "Body": 1 }, { "Body":
0 } ] not [ { "Body": 0 }, { "Body": 1 } ]; update the regions_outlive entry in
the fixture so the first element is Body(1) and the second is Body(0) so that
the lifter (llbc_lift.rs) will emit Outlives(region_a, region_b) with the
longer-lived region as the first argument and make the test
region_longer_emits_outlives_b_a reflect the intended direction.

In `@implementations/rust/provekit-walk/tests/fixtures/region_static.llbc`:
- Around line 228-271: Statement id=4 in the fixture for function give_static
incorrectly assigns a plain Copy of local 1 (type u32) into local 0 (type
&'static u32); change the second Assign element for statement id=4 so that it
produces a reference-to-promoted-constant (i.e., a Ref/Promoted or otherwise a
Literal reference with Static=true) rather than a Copy of an integer: update the
part that currently emits "Use: Copy: Local 1" to emit a reference/Literal that
matches local 0's type (&'static u32), ensuring the Assign for local 0 is
structurally a Ref to a static/promoted u32 consistent with the function
give_static signature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21439c5d-4919-4863-867f-72c60772abb1

📥 Commits

Reviewing files that changed from the base of the PR and between a585347 and fa79f02.

📒 Files selected for processing (17)
  • implementations/rust/provekit-walk/src/aliasing.rs
  • implementations/rust/provekit-walk/src/bin/walk_demo.rs
  • implementations/rust/provekit-walk/src/contract.rs
  • implementations/rust/provekit-walk/src/lib.rs
  • implementations/rust/provekit-walk/src/llbc.rs
  • implementations/rust/provekit-walk/src/llbc_calls.rs
  • implementations/rust/provekit-walk/src/llbc_lift.rs
  • implementations/rust/provekit-walk/src/marriage.rs
  • implementations/rust/provekit-walk/src/sort_translate.rs
  • implementations/rust/provekit-walk/tests/fixtures/region_id.llbc
  • implementations/rust/provekit-walk/tests/fixtures/region_id.rs
  • implementations/rust/provekit-walk/tests/fixtures/region_longer.llbc
  • implementations/rust/provekit-walk/tests/fixtures/region_longer.rs
  • implementations/rust/provekit-walk/tests/fixtures/region_static.llbc
  • implementations/rust/provekit-walk/tests/fixtures/region_static.rs
  • implementations/rust/provekit-walk/tests/fixtures/region_two_unrelated.llbc
  • implementations/rust/provekit-walk/tests/fixtures/region_two_unrelated.rs

Comment on lines +41 to +47
// Cycle detection
if let Some(aid) = adt_id {
if !visited.insert(aid) {
return false; // already visited, no interior mut found
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the cycle check path-local, not global.

visited is never unwound, so a sibling branch that reuses the same ADT id with different type arguments can be skipped incorrectly. For example, if one field visits Wrapper<u32> first, a later Wrapper<Cell<u32>> will hit Line 43 and return false before inspecting the interior-mutating generic. That under-reports PossibleAliasing for generic wrappers.

Use path-local cycle detection here: either remove the ADT id from visited on unwind, or key the visit by the fully instantiated type rather than only by def_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/aliasing.rs` around lines 41 - 47, The
cycle detection currently uses the global `visited` set keyed only by `adt_id`,
causing sibling branches with different type arguments (e.g., `Wrapper<u32>` vs
`Wrapper<Cell<u32>>`) to be skipped; make the check path-local by either
removing the `adt_id` from `visited` when unwinding this visit or by changing
the key to the fully instantiated type (include type parameters) instead of just
`def_id`; update the logic around the `if let Some(aid) = adt_id { if
!visited.insert(aid) { return false } }` block in aliasing.rs and ensure
`PossibleAliasing` detection now considers the per-path instantiation so generic
wrappers with interior-mut types are not under-reported.

Comment on lines +200 to +208
impl AliasingMemento {
pub fn to_jcs_value(&self) -> Arc<Value> {
Value::object([
("kind", Value::string("aliasing-memento")),
("formal_a", Value::string(self.formal_a.clone())),
("formal_b", Value::string(self.formal_b.clone())),
("status", Value::string(self.status.as_str())),
])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canonicalize aliasing pairs before hashing them.

check_aliasing_discharged() already treats (a,b) and (b,a) as the same relation, but to_jcs_value() preserves whichever order the producer emitted and aliasing_mementos_to_value() sorts on those raw fields. Two equivalent bundles can therefore discharge identically yet produce different autoMintedMementos bytes/CIDs. Normalize the pair before encoding/sorting, and include status in the comparator so same-name duplicates are deterministic.

Proposed fix
 impl AliasingMemento {
+    fn canonical_pair(&self) -> (&str, &str) {
+        if self.formal_a <= self.formal_b {
+            (&self.formal_a, &self.formal_b)
+        } else {
+            (&self.formal_b, &self.formal_a)
+        }
+    }
+
     pub fn to_jcs_value(&self) -> Arc<Value> {
+        let (formal_a, formal_b) = self.canonical_pair();
         Value::object([
             ("kind", Value::string("aliasing-memento")),
-            ("formal_a", Value::string(self.formal_a.clone())),
-            ("formal_b", Value::string(self.formal_b.clone())),
+            ("formal_a", Value::string(formal_a.to_string())),
+            ("formal_b", Value::string(formal_b.to_string())),
             ("status", Value::string(self.status.as_str())),
         ])
     }
 }
 
 fn aliasing_mementos_to_value(mementos: &[AliasingMemento]) -> Arc<Value> {
     let mut sorted: Vec<&AliasingMemento> = mementos.iter().collect();
     sorted.sort_by(|a, b| {
-        a.formal_a
-            .cmp(&b.formal_a)
-            .then_with(|| a.formal_b.cmp(&b.formal_b))
+        let (a1, a2) = a.canonical_pair();
+        let (b1, b2) = b.canonical_pair();
+        a1.cmp(b1)
+            .then_with(|| a2.cmp(b2))
+            .then_with(|| a.status.as_str().cmp(b.status.as_str()))
     });
     let items: Vec<Arc<Value>> = sorted.iter().map(|m| m.to_jcs_value()).collect();
     Value::array(items)
 }

Also applies to: 404-411, 544-552

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/contract.rs` around lines 200 - 208,
to_jcs_value() is emitting alias pairs in the original order which leads to
non-deterministic hashes/CIDs because (a,b) and (b,a) are considered equivalent
by check_aliasing_discharged(); update to_jcs_value() (and the related
aliasing_mementos_to_value() comparator) to canonicalize the pair ordering
(e.g., sort the two formals lexicographically) before constructing the JCS Value
and ensure the comparator/sort also includes the status field so identical-name
duplicates are deterministic, keeping the symbol names to_jcs_value(),
aliasing_mementos_to_value(), and check_aliasing_discharged() as points of
change.

Comment on lines +353 to +355
pub formal_regions: Vec<Option<String>>,
pub return_sort: Sort,
pub return_region: Option<String>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Commit the new lifetime metadata into the canonical contract value.

FunctionContractMemento now stores formal_regions and return_region, but build_value()/build_memento_value() never serialize them. After LLBC region enrichment, recomputing the CID will still hash the same bytes as a contract with no lifetime metadata, so the new C.9 data is not actually part of contract identity.

Proposed fix
 fn build_value(
     fn_name: &str,
     formals: &[String],
     formal_sorts: &[Sort],
+    formal_regions: &[Option<String>],
     return_sort: &Sort,
+    return_region: Option<&str>,
     pre: &IrFormula,
     post: &IrFormula,
     body_cid: Option<&str>,
     effects: &EffectSet,
     locus: &Locus,
     auto_minted_mementos: &[AliasingMemento],
 ) -> Arc<Value> {
     let formals_arr: Vec<Arc<Value>> = formals.iter().map(|n| Value::string(n.clone())).collect();
     let formal_sorts_arr: Vec<Arc<Value>> =
         formal_sorts.iter().map(|s| sort_to_value(s)).collect();
+    let formal_regions_arr: Vec<Arc<Value>> = formal_regions
+        .iter()
+        .map(|r| match r {
+            Some(name) => Value::string(name.clone()),
+            None => Value::null(),
+        })
+        .collect();
     let body_cid_val: Arc<Value> = match body_cid {
         Some(c) => Value::string(c.to_string()),
         None => Value::null(),
     };
+    let return_region_val: Arc<Value> = match return_region {
+        Some(r) => Value::string(r.to_string()),
+        None => Value::null(),
+    };
     Value::object([
         ("schemaVersion", Value::string("1")),
         ("kind", Value::string("function-contract")),
         ("fnName", Value::string(fn_name.to_string())),
         ("formals", Value::array(formals_arr)),
         ("formalSorts", Value::array(formal_sorts_arr)),
+        ("formalRegions", Value::array(formal_regions_arr)),
         ("returnSort", sort_to_value(return_sort)),
+        ("returnRegion", return_region_val),
         ("pre", formula_to_canonical(pre)),
         ("post", formula_to_canonical(post)),
         ("bodyCid", body_cid_val),
         ("effects", effects.to_value()),
         ("locus", locus.to_value()),
         ("autoMintedMementos", aliasing_mementos_to_value(auto_minted_mementos)),
     ])
 }

Also applies to: 486-518, 525-537

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/contract.rs` around lines 353 - 355,
FunctionContractMemento now contains new lifetime metadata fields formal_regions
and return_region but build_value() and build_memento_value() do not serialize
them, so the canonical contract bytes (and resulting CID) omit lifetime data;
update build_value() and build_memento_value() to include serializing
FunctionContractMemento.formal_regions and .return_region into the canonical
contract/memento representation (follow the existing ordering/encoding used for
other fields) so that the LLBC-enriched regions become part of the contract
identity.

Comment on lines +394 to +423
/// Check that every `Effect::PossibleAliasing` pair of formals has a
/// matching AliasingMemento in `auto_minted_mementos`. Returns
/// `Ok(())` if aliasing is fully discharged or no PossibleAliasing
/// effects are present. Returns the first undischarged pair as an error.
pub fn check_aliasing_discharged(&self) -> Result<(), OpacityError> {
for effect in &self.effects.effects {
if let Effect::PossibleAliasing { formals } = effect {
if formals.len() < 2 {
continue;
}
for i in 0..formals.len() {
for j in (i + 1)..formals.len() {
let a = &formals[i];
let b = &formals[j];
let covered = self.auto_minted_mementos.iter().any(|m| {
(m.formal_a == *a && m.formal_b == *b)
|| (m.formal_a == *b && m.formal_b == *a)
});
if !covered {
return Err(OpacityError::AliasingNotDischarged {
formal_a: a.clone(),
formal_b: b.clone(),
});
}
}
}
}
}
Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Route aliasing discharge through the verifier lookup, not only the embedded bundle.

This path only checks self.auto_minted_mementos, and OpacityMementoLookup has no aliasing hook. For the new shared-&T PossibleAliasing cases, that means a verifier-supplied aliasing proof can never unblock composition here unless the contract itself is mutated to carry it. The discharge API should go through the same pool abstraction the rest of opacity uses.

Also applies to: 608-613, 843-859

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/contract.rs` around lines 394 - 423,
The check_aliasing_discharged implementation currently only inspects
self.auto_minted_mementos and therefore never considers verifier-supplied
aliasing proofs; change it to query the canonical memento pool/lookup
abstraction (OpacityMementoLookup) used elsewhere in opacity rather than
directly iterating auto_minted_mementos: replace the coverage check in
check_aliasing_discharged with a call into the lookup (or pool) API (e.g., a
method like lookup.has_aliasing_memento(formal_a, formal_b) or equivalent) so
verifier-provided mementos are considered, and apply the same refactor to the
analogous checks mentioned in the other spots (the other aliasing-discharge
loops at the two similar sites) so all aliasing discharge paths go through the
shared lookup abstraction.

Comment on lines +321 to +345
let mut region_map: HashMap<u32, String> = HashMap::new();
if let Some(generics) = f.raw().get("generics") {
if let Some(regions) = generics.get("regions").and_then(|v| v.as_array()) {
for r in regions {
if let (Some(idx), Some(name)) = (
r.get("index").and_then(|v| v.as_u64()).map(|v| v as u32),
r.get("name").and_then(|v| v.as_str()),
) {
// TODO(#384 C.9): HRTB / late-bound regions have null names.
// When name is "'a" we use it verbatim; when null, the
// resolve_region_json fallback produces "'r<N>".
let name_str = if name == "''" || name.is_empty() {
format!("'r{}", idx)
} else {
name.to_string()
};
region_map.insert(idx, name_str.clone());
// Charon may use Body(N) where N is idx or idx+1.
// Map both to catch either encoding.
region_map.entry(idx.saturating_add(1)).or_insert_with(|| name_str);
} else if let Some(idx) = r.get("index").and_then(|v| v.as_u64()).map(|v| v as u32) {
// Null name: use fallback.
let name_str = format!("'r{}", idx);
region_map.insert(idx, name_str.clone());
region_map.entry(idx.saturating_add(1)).or_insert_with(|| name_str);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid storing 0-based and 1-based region ids in the same map.

This map aliases distinct lifetimes as soon as there is more than one region. With 'a at index 0 and 'b at index 1, Line 340 first makes key 1 -> 'a, then Line 337 for the second region overwrites key 1 -> 'b. Any Body(1) that uses the 1-based encoding now resolves to the wrong lifetime.

Use one canonical key space here and make the fallback explicit during lookup instead of inserting both conventions into the same HashMap.

Comment on lines +398 to +406
let pre_with_regions = if region_facts.is_empty() {
pre_formula.clone()
} else if matches!(&pre_formula, IrFormula::Atomic { name, args } if name == "true" && args.is_empty()) {
simplify_conjunction(region_facts)
} else {
let mut operands = vec![pre_formula.clone()];
operands.extend(region_facts);
IrFormula::And { operands }
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate the new Outlives facts into the post contract too.

post_formula is finalized before region_facts exist, so these signature-level lifetime constraints only land in pre_with_regions. That breaks the existing invariant in this function that pre contributions still hold at return, and it drops Outlives facts from the return-side contract.

Proposed fix
-    let post_formula = simplify_conjunction(post_contribs);
+    // Defer post_formula until region_facts are available.
...
     let pre_with_regions = if region_facts.is_empty() {
         pre_formula.clone()
     } else if matches!(&pre_formula, IrFormula::Atomic { name, args } if name == "true" && args.is_empty()) {
         simplify_conjunction(region_facts)
     } else {
         let mut operands = vec![pre_formula.clone()];
-        operands.extend(region_facts);
+        operands.extend(region_facts.clone());
         IrFormula::And { operands }
     };
+
+    post_contribs.extend(region_facts);
+    let post_formula = simplify_conjunction(post_contribs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/llbc_lift.rs` around lines 398 - 406,
pre_with_regions currently merges region_facts into the pre contract but
post_formula is finalized earlier so Outlives facts are not propagated to the
return-side; mirror the same merging logic for the post contract: build a
post_with_regions by checking if region_facts.is_empty() then clone
post_formula, else if post_formula matches IrFormula::Atomic { name == "true",
args.is_empty() } then use simplify_conjunction(region_facts), else create
operands = vec![post_formula.clone()] ; operands.extend(region_facts) and set
IrFormula::And { operands }; then use post_with_regions where post_formula was
previously used so Outlives facts are preserved at return.

Comment on lines +215 to +223
// Effect-based classification: PossibleAliasing is an LLBC-only effect
// (AST cannot see borrow shape). When formulas agree but LLBC carries
// PossibleAliasing that AST doesn't, classify as LlbcExtra(BorrowState).
if agreement == LayerAgreement::Identical {
let llbc_has_aliasing = llbc.effects.effects.iter().any(|e| matches!(e, Effect::PossibleAliasing { .. }));
let ast_has_aliasing = ast.effects.effects.iter().any(|e| matches!(e, Effect::PossibleAliasing { .. }));
if llbc_has_aliasing && !ast_has_aliasing {
return LayerAgreement::LlbcExtra(LlbcExtraCategory::BorrowState);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Borrow-state differences are classified here, but still lost from the merged contract.

This new branch correctly notices LLBC-only PossibleAliasing, but marry() still starts from ast.clone(). The substrate-facing merged contract therefore drops the LLBC-only borrow metadata/evidence (PossibleAliasing, auto_minted_mementos, and the new region fields), so the new C.8/C.9 information never reaches the record that downstream composition actually consumes.

Suggested direction
 let mut merged = ast.clone();
+merged.effects = merge_effect_sets(&ast.effects, &llbc.effects);
+merged.auto_minted_mementos = llbc.auto_minted_mementos.clone();
+merged.formal_regions = llbc.formal_regions.clone();
+merged.return_region = llbc.return_region.clone();

You’ll likely also want a deterministic merge for EffectSet plus coverage around married contracts that carry PossibleAliasing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/src/marriage.rs` around lines 215 - 223,
The Identical-agreement branch detects LLBC-only PossibleAliasing but marry()
still begins from ast.clone(), dropping LLBC borrow metadata; change marry() so
when you return LayerAgreement::LlbcExtra(LlbcExtraCategory::BorrowState) you
build the merged contract by starting from a merge of ast and llbc (not
ast.clone()), deterministically combining EffectSet (merge llbc.effects into the
merged.effects with a stable ordering), and also copying LLBC-only fields
(auto_minted_mementos, the new region fields, and any PossibleAliasing effects)
into the merged contract; update or add tests around
marry()/LayerAgreement::LlbcExtra and Effect::PossibleAliasing to cover married
contracts that must preserve LLBC borrow evidence.

Comment on lines +57 to +66
"regions_outlive": [
[
{
"Body": 0
},
{
"Body": 1
}
]
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the regions_outlive consumption in the lifter and region-related code.
rg -n --type=rust "regions_outlive" -A 5 -B 2

Repository: TSavo/provekit

Length of output: 2487


🏁 Script executed:

#!/bin/bash
rg -n --type=rust "region_longer|Outlives|outlives" -A 5 -B 2

Repository: TSavo/provekit

Length of output: 22374


🏁 Script executed:

fd -type f "region_longer" --extension rs

Repository: TSavo/provekit

Length of output: 229


🏁 Script executed:

fd "region_longer" -name "*.rs"

Repository: TSavo/provekit

Length of output: 289


🏁 Script executed:

fd "region_longer" --type f

Repository: TSavo/provekit

Length of output: 194


🏁 Script executed:

cat implementations/rust/provekit-walk/tests/fixtures/region_longer.rs

Repository: TSavo/provekit

Length of output: 126


regions_outlive pair encodes the wrong outlives direction.

The Rust source is longer<'a, 'b: 'a>, meaning 'b (index 1) outlives 'a (index 0). In llbc_lift.rs (lines 367–368), the semantics are explicit: each pair encodes "region_a outlives region_b", and the lifter emits Outlives(region_a, region_b) where the first argument is the longer-lived region. The correct encoding for 'b: 'a is [Body(1), Body(0)], but the fixture has [Body(0), Body(1)], which emits Outlives('a, 'b) instead. The test region_longer_emits_outlives_b_a (line 3037) has a name that explicitly promises the correct direction but only validates that both regions are present, not their order—allowing this bug to pass undetected.

🐛 Proposed fix
-          "regions_outlive": [
-            [
-              {
-                "Body": 0
-              },
-              {
-                "Body": 1
-              }
-            ]
-          ],
+          "regions_outlive": [
+            [
+              {
+                "Body": 1
+              },
+              {
+                "Body": 0
+              }
+            ]
+          ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/tests/fixtures/region_longer.llbc` around
lines 57 - 66, The fixture encodes the outlives pair in the wrong direction: in
the Rust source longer<'a, 'b: 'a> region index 1 ('b) outlives index 0 ('a), so
the "regions_outlive" pair should be [ { "Body": 1 }, { "Body": 0 } ] not [ {
"Body": 0 }, { "Body": 1 } ]; update the regions_outlive entry in the fixture so
the first element is Body(1) and the second is Body(0) so that the lifter
(llbc_lift.rs) will emit Outlives(region_a, region_b) with the longer-lived
region as the first argument and make the test region_longer_emits_outlives_b_a
reflect the intended direction.

Comment on lines +228 to +271
"id": 4,
"kind": {
"Assign": [
{
"kind": {
"Local": 0
},
"ty": {
"Untagged": {
"Ref": [
{
"Static": true
},
{
"Untagged": {
"Literal": {
"UInt": "U32"
}
}
},
"Shared"
]
}
}
},
{
"Use": {
"Copy": {
"kind": {
"Local": 1
},
"ty": {
"Untagged": {
"Literal": {
"UInt": "U32"
}
}
}
}
}
}
]
},
"comments_before": []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any test references region_static and what fields it inspects.
rg -n "region_static" -A 10 -B 2

# Check if the body traversal in llbc_lift.rs processes local assignments and validates type consistency.
rg -n --type=rust "body\|locals\|StorageLive\|Assign" implementations/rust/provekit-walk/src/llbc_lift.rs | head -40

Repository: TSavo/provekit

Length of output: 4784


🏁 Script executed:

#!/bin/bash
# Look at the lift_llbc_function implementation to see if it processes the body
rg -n "fn lift_llbc_function" implementations/rust/provekit-walk/src/llbc_lift.rs -A 30

# Check if there's any type validation or consistency checking
rg -n "type.*valid\|check.*type\|type.*check" implementations/rust/provekit-walk/src/llbc_lift.rs -i

Repository: TSavo/provekit

Length of output: 2953


🏁 Script executed:

#!/bin/bash
# Get the full implementation of lift_llbc_function_with_registry
rg -n "fn lift_llbc_function_with_registry" implementations/rust/provekit-walk/src/llbc_lift.rs -A 200 | head -250

Repository: TSavo/provekit

Length of output: 9301


🏁 Script executed:

#!/bin/bash
# Check if there's any type validation logic in body processing
rg -n "type.*mismatch\|mismatch.*type\|type.*error\|invalid.*type" implementations/rust/provekit-walk/src/llbc_lift.rs -i

# Check the detect_effects_llbc function to understand what it does
rg -n "fn detect_effects_llbc" implementations/rust/provekit-walk/src/llbc_lift.rs -A 20

# Check for Assign statement handling in the body processing
rg -n "Assign\|statement" implementations/rust/provekit-walk/src/llbc_lift.rs | head -30

Repository: TSavo/provekit

Length of output: 3833


Body statement id=4 has a structural type mismatch in the fixture.

Statement id=4 copies local 1 (type u32) into local 0 (type &'static u32). This violates LLBC type consistency: a value of type u32 cannot be assigned to a reference-typed slot without a Ref operation. For the source fn give_static() -> &'static u32 { &7 }, a structurally valid body would use a promoted constant reference, not a plain Copy of an integer value.

The test region_static_emits_static_region() only validates signature-level region extraction (formals, formal_regions, return_region) and never traverses the function body. The lifter's body-processing functions extract patterns and effects but do not perform type consistency validation, so this mismatch is benign in practice. However, the fixture should be corrected to emit valid LLBC.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementations/rust/provekit-walk/tests/fixtures/region_static.llbc` around
lines 228 - 271, Statement id=4 in the fixture for function give_static
incorrectly assigns a plain Copy of local 1 (type u32) into local 0 (type
&'static u32); change the second Assign element for statement id=4 so that it
produces a reference-to-promoted-constant (i.e., a Ref/Promoted or otherwise a
Literal reference with Static=true) rather than a Copy of an integer: update the
part that currently emits "Use: Copy: Local 1" to emit a reference/Literal that
matches local 0's type (&'static u32), ensuring the Assign for local 0 is
structurally a Ref to a static/promoted u32 consistent with the function
give_static signature.

…checks

Both PR #419 (aliasing + PossibleAliasing) and main (pin-invariant + PinnedReference,
drop effects) added to the Effect enum and composition logic. Semantic resolution:
keep both Effect::PossibleAliasing and Effect::PinnedReference in the non_opacity_pure
pattern matches, as both are opacity effects that must be discharged before composition.

Conflict 1 (discharge check): HEAD added PossibleAliasing discharge, main added
PinnedReference discharge — kept both branches.

Conflicts 2-4 (pattern matches): HEAD's PossibleAliasing + main's PinnedReference
merged into single unified list for outer_non_opacity_pure and inner_non_opacity_pure.
@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Merge conflict resolved: contract.rs had conflicting discharge checks for PossibleAliasing (HEAD) and PinnedReference (main). Kept both: OpacityMementoLookup trait has has_aliasing_memento and check_opacity_effects handles both Effect::PossibleAliasing (auto-discharged) and Effect::PinnedReference (pool lookup). Non-opacity-pure pattern matches updated to include all four opacity effects. Build and tests pass.

@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Superseded by per-issue PRs that landed via the work-order template: #439 (AliasingMemento discharge / #416), #441 (region-quantifier composition / #412), #444 (marriage hookup / #413), #446 (C.9 integration test scaffolding / #414), and the C.8 lifter side via the same family. Content is now fully duplicated on main; closing without merge to avoid conflict noise.

@TSavo
Copy link
Copy Markdown
Owner Author

TSavo commented May 6, 2026

Superseded by PR #458 (clean rebase on current main). Closing in favor of the replacement PR.

ConstCorrectness pushed a commit to ConstCorrectness/provekit that referenced this pull request May 6, 2026
… pool

- invokeSolver() now uses the worker pool instead of spawn-per-call
- Each (check-sat) query uses (push)/(pop) for scope isolation
- Eliminates startup overhead and process contention on shared CI runners
- Tests now pass consistently without timeout flakes

Relates: TSavo#429 TSavo#419 TSavo#439
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.

[walk] C.8 Aliasing: Effect::PossibleAliasing + auto-mint Disjoint AliasingMemento for &mut formals (#384 C.8)

2 participants