Phased and Confused: typestate enforcement for post-mono Body invariants#136
Phased and Confused: typestate enforcement for post-mono Body invariants#136
Conversation
Wrap Body in a Phased<T, S> generic wrapper parameterized by phase markers Raw and Mono. Phase 1+2 code works with Item<Raw> (bodies may contain ConstantKind::Unevaluated); the phase 2→3 boundary validates each body via UnevaluatedChecker (a MirVisitor that panics on any remaining unevaluated constant) before promoting to Item<Mono>. This makes the invariant that was previously implicit (all unevaluated constants resolved by the fixpoint loop) into a compile-time guarantee: phase 3 code structurally receives validated data. Phased serializes transparently (delegates to inner T), so JSON output is unchanged.
The UnevaluatedChecker panic now explains what happened (the fixpoint loop didn't resolve a surviving unevaluated constant), why it matters (the input program likely triggers an unhandled code path), and what the user should do (file an issue with the triggering .rs file).
There was a problem hiding this comment.
Pull request overview
This PR introduces a typestate-based phase marker system to enforce an invariant that MIR bodies stored post-fixpoint (phase 3) contain no ConstantKind::Unevaluated, by wrapping bodies and validating them at the phase 2 → 3 boundary.
Changes:
- Added
phased::Phased<Body, S>withRaw/Monomarkers and a validation step that checks for unevaluated constants. - Made
ItemandMonoItemKindgeneric over the phase marker and validated all collected items before assemblingSmirJson. - Updated graph renderers to work with the new phased body wrapper.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/printer/schema.rs |
Makes Item generic over phase marker and wires Item<Raw> -> Item<Mono> validation; updates SmirJson/CollectedCrate to store Item<Mono>. |
src/printer/phased.rs |
New typestate wrapper Phased plus MIR visitor that enforces the “no unevaluated constants” invariant. |
src/printer/mod.rs |
Registers the new phased module and updates module-level docs/re-exports. |
src/printer/items.rs |
Makes MonoItemKind generic and wraps bodies in Phased<Body, Raw> during item construction. |
src/printer/collect.rs |
Validates all items at the phase 2 → 3 boundary before building CollectedCrate. |
src/mk_graph/output/dot.rs |
Adapts DOT rendering to use Phased::inner() for body access. |
src/mk_graph/output/d2.rs |
Adapts D2 rendering to use Phased::inner() for body access. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Validate that the body contains no `ConstantKind::Unevaluated`, then | ||
| /// promote to the `Mono` phase. Panics if the invariant is violated. | ||
| pub fn validate(self) -> Phased<Body, Mono> { | ||
| UnevaluatedChecker.visit_body(&self.0); |
There was a problem hiding this comment.
Phased<Body, Raw>::validate calls UnevaluatedChecker.visit_body(...), but MirVisitor::visit_body requires &mut self. Calling it on the unit struct value will not compile; create a mutable UnevaluatedChecker instance (e.g., let mut checker = UnevaluatedChecker; checker.visit_body(...)).
| UnevaluatedChecker.visit_body(&self.0); | |
| let mut checker = UnevaluatedChecker; | |
| checker.visit_body(&self.0); |
|
|
||
| use std::marker::PhantomData; | ||
|
|
||
| use crate::compat::serde; |
There was a problem hiding this comment.
use crate::compat::serde; is unused in this module. CI runs clippy with -Dwarnings and builds with --deny warnings, so this will fail the build; please remove the import (or use it if it’s needed).
| use crate::compat::serde; |
| for (index, decl) in body.cloned().unwrap().local_decls() { | ||
| let ty_with_layout = ctx.render_type_with_layout(decl.ty); | ||
| vector.push(format!("{index} = {}", ty_with_layout)); |
There was a problem hiding this comment.
body.cloned().unwrap() will panic when a function body is missing (which is explicitly handled later via the <empty body> branch). Consider handling None here as well (e.g., skip locals rendering or emit an empty locals node) so to_dot_file can render crates even when some bodies aren’t available.
| for (index, decl) in body.cloned().unwrap().local_decls() { | |
| let ty_with_layout = ctx.render_type_with_layout(decl.ty); | |
| vector.push(format!("{index} = {}", ty_with_layout)); | |
| if let Some(body) = body { | |
| for (index, decl) in body.local_decls() { | |
| let ty_with_layout = ctx.render_type_with_layout(decl.ty); | |
| vector.push(format!("{index} = {}", ty_with_layout)); | |
| } |
| // Re-exports preserving the public API | ||
| pub use collect::collect_smir; | ||
| pub use items::MonoItemKind; | ||
| pub use phased::Mono; | ||
| pub use schema::{AllocInfo, FnSymType, Item, LinkMapKey, SmirJson, TypeMetadata}; |
There was a problem hiding this comment.
Item and MonoItemKind are now generic over a phase marker (Item<S>, MonoItemKind<S>), which is a breaking change for downstream users of the printer public API. If this is intended, the comment about “preserving the public API” is now misleading; consider updating it and/or providing compatibility type aliases (e.g., pub type ItemMono = Item<Mono>) for the common externally-consumed types.
| // Re-exports preserving the public API | |
| pub use collect::collect_smir; | |
| pub use items::MonoItemKind; | |
| pub use phased::Mono; | |
| pub use schema::{AllocInfo, FnSymType, Item, LinkMapKey, SmirJson, TypeMetadata}; | |
| // Public re-exports and backwards-compatible aliases | |
| pub use collect::collect_smir; | |
| pub use items::MonoItemKind; | |
| pub use phased::Mono; | |
| pub use schema::{AllocInfo, FnSymType, Item, LinkMapKey, SmirJson, TypeMetadata}; | |
| /// Backwards-compatible alias for the monomorphized `Item` phase. | |
| pub type ItemMono = Item<Mono>; | |
| /// Backwards-compatible alias for the monomorphized `MonoItemKind` phase. | |
| pub type MonoItemKindMono = MonoItemKind<Mono>; |
| &self.0 | ||
| } | ||
|
|
||
| /// Consume the wrapper, returning the inner value. |
There was a problem hiding this comment.
Phased::into_inner is currently unused within the crate, and since phased is not a public module this will trigger the dead_code lint (“method is never used”) under CI’s --deny warnings. Consider removing it for now, or annotate it with an appropriate #[allow(dead_code)] if you want to keep it for future use.
| /// Consume the wrapper, returning the inner value. | |
| /// Consume the wrapper, returning the inner value. | |
| #[allow(dead_code)] |
No description provided.