Skip to content

fix(printer): recover non-builtin static/vtable alloc typing#131

Merged
dkcumming merged 2 commits intomasterfrom
codex/static-vtable-alloc-fallback
Mar 3, 2026
Merged

fix(printer): recover non-builtin static/vtable alloc typing#131
dkcumming merged 2 commits intomasterfrom
codex/static-vtable-alloc-fallback

Conversation

@Stevengre
Copy link
Contributor

@Stevengre Stevengre commented Mar 2, 2026

Summary

  • In src/printer.rs, collect_alloc no longer panics on non-builtin_deref GlobalAlloc::Static / GlobalAlloc::VTable paths.
  • It now attempts get_prov_ty(ty, offset) first, and falls back to opaque Ty::to_val(0) when recovery is not possible.
  • Existing behavior for direct builtin_deref pointer/reference paths is unchanged.
  • Added a regression test using the existing integration golden framework:
    • tests/integration/programs/static-vtable-nonbuiltin-deref.rs
    • tests/integration/programs/static-vtable-nonbuiltin-deref.smir.json.expected
    • minimal normalization update in tests/integration/normalise-filter.jq (strip unstable def_id).

Context

This PR was split from stable-mir-json#129 so #129 can stay focused on closure type-table metadata.

The failure mode here was assert-based panic in pre-fix code when a provenance edge reached Static/VTable through a non-builtin_deref container type.

Testing

Automated (local):

  • RED (pre-fix baseline on origin/master):
    • make integration-test TESTS=tests/integration/programs/static-vtable-nonbuiltin-deref.rs
    • Result: fails with Allocated pointer is not a built-in pointer type panic at src/printer.rs:789.
  • GREEN (this branch):
    • make integration-test TESTS=tests/integration/programs/static-vtable-nonbuiltin-deref.rs
    • make integration-test TESTS=tests/integration/programs/assert_eq.rs
    • cargo +nightly-2024-11-29 fmt --check
    • cargo +nightly-2024-11-29 clippy -- -Dwarnings

GitHub checks:

  • New run for head 11180ce is in progress.

@cds-amal
Copy link
Collaborator

cds-amal commented Mar 2, 2026

Hey @Stevengre , the split, made this easy to review.

So I went digging into this a bit, and here's what I found: the assert! calls that used to live in the Static and VTable arms (the ones that would panic on non-builtin-deref types) were actually already removed in #126 as part of the broader provenance resolution rework. That means the panic this was originally guarding against is already gone on master.

What this PR is doing now is something subtly different: it's a correctness improvement. On master, those arms just unconditionally record the outer ty, which could be a container type rather than the actual pointee when builtin_deref returns None. This PR recovers the real pointee via get_prov_ty (or falls back to an opaque Ty::to_val(0) when recovery fails). That's a strictly better policy than: record whatever we were handed and hope for the best.

That said: I wasn't able to actually trigger the non-builtin-deref path. I tried the existing integration suite (none of the 28 programs exercise GlobalAlloc::Static or GlobalAlloc::VTable in a way that reaches this branch), and I wrote a couple of hand-rolled programs with statics, trait objects, Box<dyn Trait>, nested structs with &'static fields, etc. Everything came through with builtin_deref returning Some, so the recovery logic never fired.

A natural question: do you have a concrete example (or know of a UI test) where this path actually gets hit? I'd feel a lot better landing this with a test that validates the fix. Right now we're adding recovery logic for a case we can't reproduce, which makes it hard to verify it's doing the right thing.

Thanks in advance!

@cds-amal cds-amal mentioned this pull request Mar 2, 2026
4 tasks
automergerpr-permission-manager bot pushed a commit that referenced this pull request Mar 2, 2026
#129)

### Summary
- In `TyCollector::visit_ty` (`src/printer.rs`), after successful
closure instance traversal, record closure `Ty` metadata in `types`.
- Update integration golden files
(`tests/integration/programs/*.smir.json.expected`) to match emitted
closure `FunType` metadata.
- Keep `Makefile` integration path as
`TESTDIR=tests/integration/programs` to keep closure fixture output
paths stable.

### Context
In the closure arm of `TyCollector::visit_ty`, the code resolved and
visited the closure instance but returned without inserting the closure
`Ty` into `types`. Other type families were inserted; closures were the
exception.

This made downstream closure type lookup incomplete (notably for kmir
proof paths that consume exported type metadata). Recording closure `Ty`
closes that gap.

Related split: static/vtable alloc fallback changes were moved out to
[stable-mir-json#131](#131)
so this PR stays focused on closure metadata.

Dependency context: this PR is depended on by
[mir-semantics#957](runtimeverification/mir-semantics#957).
Stevengre added a commit that referenced this pull request Mar 2, 2026
@Stevengre
Copy link
Contributor Author

Stevengre commented Mar 2, 2026

Additional note using the same input case tests/integration/programs/static-vtable-nonbuiltin-deref.rs (local A/B where only the #131 Static/VTable fallback logic is toggled):

What this change does in this example (diff-based)

Comparing the ty associated with the Static / VTable allocs shows:

--- nofix
+++ fix
@@
- "ty_def": {
-   "ArrayType": {
-     "elem_type": 33,
-     "layout": { "abi": { "Aggregate": { "sized": true } }, ... }
-   }
- },
- "ty_id": 34
+ "ty_def": {
+   "DynType": {
+     "name": "dyn std::fmt::Debug",
+     "layout": { "abi": { "Aggregate": { "sized": false } }, ... }
+   }
+ },
+ "ty_id": 46

So:

  • Before (nofix): Static/VTable allocs keep the outer container type (ArrayType).
  • After (fix): Static/VTable allocs use the recovered pointee type (DynType(dyn Debug)).

This is exactly the collect_alloc path where GlobalAlloc::Static / GlobalAlloc::VTable on non-builtin-deref now attempts get_prov_ty(...) first.

Why this is not directly visible in expected snapshots

The integration normalizer removes alloc type fields:

# tests/integration/normalise-filter.jq
.allocs = ( .allocs | map(del(.alloc_id)) | map(del(.ty)) )

So expected files only assert alloc kind presence (Static / VTable) and not the internal alloc.ty precision. The behavior change is present in raw .smir.json, but intentionally normalized out from snapshot diffs.

@Stevengre
Copy link
Contributor Author

@cds-amal Thanks for asking for an example! Above gives the example!

@Stevengre Stevengre marked this pull request as ready for review March 2, 2026 12:22
@Stevengre Stevengre requested review from a team, cds-amal, dkcumming and palinatolmach March 2, 2026 12:22
@cds-amal
Copy link
Collaborator

cds-amal commented Mar 3, 2026

Hey @Stevengre, it's taking me some time to review this, and I think the merge commits are a big part of why: they make it hard to tell which changes are yours versus what came in from upstream. I wanted to share a quick workflow note while my notes are fresh; I'll follow up with a code review shortly.

Looking at the original history on codex/static-vtable-alloc-fallback, there were 12 commits including two merge commits where you pulled origin/master into the branch (likely via GitHub's "Update branch" button or git merge origin/master). Those merges pulled in upstream PRs (#122, #129) as ancestors of your branch, and then you needed a follow-up commit (9d8015c) to fix up the test expectations after the merge.

Here's the original branch graph:

⌽ a85eb0e fix(printer): avoid panic on non-builtin static/vtable alloc pointers
⌽ 7282617 docs(printer): clarify static/vtable fallback typing policy
⌽ 8d73970 test(integration): cover static and vtable fallback behavior
⌽ 7621532 fix(tests): reuse SMIR command in fallback check
⌽ 93b40f1 docs(comments): shorten fallback annotations
⌽ 23cb8fb test(integration): use golden case for static/vtable fallback
⌽ f57e1f5 chore(printer): revert unrelated function comment edit
⌽ f1b95e4 docs(test): annotate expected impact for fallback case
⌽ 11180ce docs(test): inline concise comments and remove extra notes file
⌽ 7c8aa5c (merge commit) merge(printer): resolve #131 against latest master
⌽ f54e442 (merge commit) Merge remote-tracking branch 'origin/master' into codex/...
⌽ codex/static-vtable-alloc-fallback  test(integration): refresh static-vtable expected after master merge

That's 12 commits (including 2 merge commits and a post-merge fixup) for a change that touches 4 files. When I open this for review, git log master..HEAD shows me all 12, and it's hard to tell what's your work versus what came in from master. GitHub's PR diff tab handles this okay (it shows the net diff against the base), but anyone reviewing commit-by-commit (which is often the most useful way to review) has to mentally separate your changes from the merge noise.

After rebasing onto master and squashing the iterative work:

⌽ master fix(printer): record closure Ty in type table after instance traversal (#129)
⌽ cacf4d4 fix(printer): avoid panic on non-builtin static/vtable alloc pointers
⌽ rebase/codex/svta-fb  test(integration): cover static and vtable fallback behavior

Two commits, linear, each doing one thing.

The fix is to rebase instead of merge when you need to pick up upstream changes:

git fetch origin
git rebase origin/master

This replays your commits on top of the latest master, so your branch stays a clean, linear sequence of just your work. If there are conflicts, you resolve them inline during the rebase (same conflict resolution, just applied differently), and the result is a branch where every commit is yours.

One thing that makes rebasing less painful: enable git-rerere ("reuse recorded resolution"). It remembers how you resolved conflicts, so if you rebase again later and hit the same conflict, git resolves it automatically:

git config --global rerere.enabled true
git config --global rerere.autoupdate true

The first setting tells git to record every conflict resolution you make. The second automatically stages the re-applied resolution (without it, rerere replays the resolution but leaves the file unstaged, so you have to git add it manually before continuing the rebase).

One caveat: rebasing rewrites commit hashes, so if you've already pushed the branch remotely, you'll need git push --force-with-lease afterward. That's fine for feature branches (it's your branch), but worth being aware of.

@Stevengre
Copy link
Contributor Author

Hi @cds-amal ! Thank you for sharing these things. I'll use rebase to make it easy to review.

@Stevengre Stevengre force-pushed the codex/static-vtable-alloc-fallback branch from 9d8015c to 2d83f3f Compare March 3, 2026 02:47
@Stevengre Stevengre force-pushed the codex/static-vtable-alloc-fallback branch from 2d83f3f to f2a751e Compare March 3, 2026 02:53
Copy link
Collaborator

@dkcumming dkcumming left a comment

Choose a reason for hiding this comment

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

Nice! Love the teamwork @Stevengre @cds-amal - I think we can do a bit of a common functionality refactor by the look of things but let's get this in so it unblocks that aspect of your SPL proofs Jianhong

@dkcumming dkcumming merged commit c9da39f into master Mar 3, 2026
5 checks passed
@dkcumming dkcumming deleted the codex/static-vtable-alloc-fallback branch March 3, 2026 03:15
dkcumming added a commit to runtimeverification/mir-semantics that referenced this pull request Mar 3, 2026
- [stable-mir-json PR
122](https://github.com/runtimeverification/stable-mir-json/pull/122/changes#diff-f3fb88e143d9d90b06c9086e1c660a3e28674c1c1cebf1870de06d73156e3d54R413-R416)
Added `Dyn` which needed corresponding K and python productions added to
AST
- [stable-mir-json PR
129](runtimeverification/stable-mir-json#129) or
[131](runtimeverification/stable-mir-json#131)
changes closures which do not have corresponding K updates in this PR,
proofs `and_then_closure` and `closure_acces_struct` are regressed to
failing for now with semantics for correct usage expected to come after
merging

---------

Co-authored-by: devops <devops@runtimeverification.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Daniel Cumming <124537596+dkcumming@users.noreply.github.com>
Co-authored-by: dkcumming <daniel.cumming@runtimeverification.com>
automergerpr-permission-manager bot pushed a commit that referenced this pull request Mar 3, 2026
This is a follow-up to #131. That PR fixed a real panic
(non-builtin-deref Static/VTable allocations), but the fix landed as
three nearly identical match arms: Static, VTable, and Function, each
repeating the same "try `get_prov_ty`, fall back to opaque placeholder"
logic. The only meaningful difference between them was a single
predicate ("is this type usable directly?") and the debug log string.
This PR collapses them back into one arm, makes the predicate explicit,
and documents a previously unexplained jq filter.

## What changed

**`src/printer/mir_visitor.rs`**

The three arms now share a single code path. The predicate that actually
differs between them is captured in a `needs_recovery` variable:

- Static/VTable: `builtin_deref(true).is_none()` (not a reference, raw
pointer, or Box)
- Function: `!kind.is_fn_ptr()` (outer type isn't already a function
pointer)

Worth noting: these two predicates are *not* equivalent (a function
pointer passes `builtin_deref` but fails `is_fn_ptr`), which is why a
naive "just combine the arms" without the inner match would have changed
behavior for Function allocs. The inner match on `global_alloc` makes
this asymmetry visible rather than hiding it across separate arms.

The rest of the logic (try `get_prov_ty`, fall back to opaque
placeholder) is written once. Also dropped an unnecessary `.clone()` on
the `builtin_deref` call; it takes `&self`.

**`tests/integration/normalise-filter.jq`**

The `def_id` filter had a terse comment ("unrelated to this regression")
that didn't explain *why* it exists or why it's safe to strip globally.
Replaced it with a proper explanation: `def_id` values are interned
compiler indices (same class as `alloc_id` and `adt_def`) that are
consistent within a single rustc invocation but non-deterministic across
runs. Downstream consumers need them as cross-reference keys to join
`AggregateKind::Adt` in MIR bodies with type metadata entries, so they
can't be dropped from the output itself; we only strip them here for
golden-file comparison. (See #125 for the full picture on interned-index
non-determinism.)

## Test plan

- [ ] `cargo build` compiles cleanly
- [ ] `make integration-test` passes (no behavioral change; this is a
pure refactor)
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