Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/mk_graph/output/d2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl SmirJson {
for item in self.items {
match item.mono_item_kind {
MonoItemKind::MonoItemFn { name, body, .. } => {
render_d2_function(&name, body.as_ref(), &ctx, &mut output);
render_d2_function(&name, body.as_ref().map(|b| b.inner()), &ctx, &mut output);
}
MonoItemKind::MonoItemGlobalAsm { asm } => {
render_d2_asm(&asm, &mut output);
Expand Down
5 changes: 3 additions & 2 deletions src/mk_graph/output/dot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl SmirJson {
for item in self.items {
match item.mono_item_kind {
MonoItemKind::MonoItemFn { name, body, id: _ } => {
let body = body.as_ref().map(|b| b.inner());
let mut c = graph.cluster();
c.set_label(&name_lines(&name));
c.set_style(Style::Filled);
Expand All @@ -81,7 +82,7 @@ impl SmirJson {
let mut local_node = c.node_auto();
let mut vector: Vec<String> = vec![];
vector.push(String::from("LOCALS"));
for (index, decl) in body.clone().unwrap().local_decls() {
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));
Comment on lines +85 to 87
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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));
}

Copilot uses AI. Check for mistakes.
}
Expand Down Expand Up @@ -287,7 +288,7 @@ impl SmirJson {
}
};

if let Some(ref body) = body {
if let Some(body) = body {
add_call_edges(&mut graph, 0, &body.blocks);
}
}
Expand Down
17 changes: 11 additions & 6 deletions src/printer/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use stable_mir::CrateDef;

use super::items::{get_foreign_module_details, mk_item};
use super::mir_visitor::{maybe_add_to_link_map, BodyAnalyzer, UnevalConstInfo};
use super::phased::Raw;
use super::schema::{
AllocInfo, AllocMap, CollectedCrate, DerivedInfo, Item, LinkMap, SmirJson, SmirJsonDebugInfo,
SpanMap,
Expand Down Expand Up @@ -53,7 +54,7 @@ fn warn_missing_body(mono_item: &MonoItem) {
}
}

fn collect_items(tcx: TyCtxt<'_>) -> HashMap<String, (MonoItem, Item)> {
fn collect_items(tcx: TyCtxt<'_>) -> HashMap<String, (MonoItem, Item<Raw>)> {
// get initial set of mono_items
let items = mono_collect(tcx);
items
Expand All @@ -73,7 +74,7 @@ fn enqueue_unevaluated_consts(
tcx: TyCtxt<'_>,
discovered: Vec<UnevalConstInfo>,
known_names: &mut HashSet<String>,
pending: &mut HashMap<String, (MonoItem, Item)>,
pending: &mut HashMap<String, (MonoItem, Item<Raw>)>,
unevaluated_consts: &mut HashMap<stable_mir::ty::ConstDef, String>,
) {
for info in discovered {
Expand All @@ -98,7 +99,7 @@ fn enqueue_unevaluated_consts(
/// this phase, then dropped; only the `Item` survives into `CollectedCrate`.
fn collect_and_analyze_items(
tcx: TyCtxt<'_>,
initial_items: HashMap<String, (MonoItem, Item)>,
initial_items: HashMap<String, (MonoItem, Item<Raw>)>,
) -> (CollectedCrate, DerivedInfo) {
let mut calls_map: LinkMap = HashMap::new();
let mut visited_allocs = AllocMap::new();
Expand All @@ -107,8 +108,8 @@ fn collect_and_analyze_items(
let mut unevaluated_consts: HashMap<stable_mir::ty::ConstDef, String> = HashMap::new();

let mut known_names: HashSet<String> = initial_items.keys().cloned().collect();
let mut pending: HashMap<String, (MonoItem, Item)> = initial_items;
let mut all_items: Vec<Item> = Vec::new();
let mut pending: HashMap<String, (MonoItem, Item<Raw>)> = initial_items;
let mut all_items: Vec<Item<Raw>> = Vec::new();

while let Some((_name, (mono_item, item))) = take_any(&mut pending) {
maybe_add_to_link_map(tcx, &mono_item, &mut calls_map);
Expand Down Expand Up @@ -142,9 +143,13 @@ fn collect_and_analyze_items(
all_items.push(item);
}

// Phase 2 -> 3 boundary: validate that all bodies are free of
// ConstantKind::Unevaluated before handing them to phase 3.
let validated_items: Vec<_> = all_items.into_iter().map(|i| i.validate()).collect();

(
CollectedCrate {
items: all_items,
items: validated_items,
unevaluated_consts,
},
DerivedInfo {
Expand Down
14 changes: 8 additions & 6 deletions src/printer/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,23 @@ use stable_mir::{CrateDef, CrateItem};

use crate::compat::bridge::mono_instance;

use super::phased::{Phased, Raw};
use super::schema::{BodyDetails, ForeignItem, ForeignModule, GenericData, Item, ItemDetails};

#[derive(Serialize, Clone)]
pub enum MonoItemKind {
#[serde(bound = "")]
pub enum MonoItemKind<S> {
MonoItemFn {
name: String,
id: stable_mir::DefId,
body: Option<Body>,
body: Option<Phased<Body, S>>,
},
MonoItemStatic {
name: String,
id: stable_mir::DefId,
allocation: Option<Allocation>,
#[serde(skip)]
body: Option<Body>,
body: Option<Phased<Body, S>>,
},
MonoItemGlobalAsm {
asm: String,
Expand Down Expand Up @@ -73,7 +75,7 @@ fn get_item_details(
}
}

pub(super) fn mk_item(tcx: TyCtxt<'_>, item: MonoItem, sym_name: String) -> (MonoItem, Item) {
pub(super) fn mk_item(tcx: TyCtxt<'_>, item: MonoItem, sym_name: String) -> (MonoItem, Item<Raw>) {
match item {
MonoItem::Fn(inst) => {
let id = inst.def.def_id();
Expand All @@ -89,7 +91,7 @@ pub(super) fn mk_item(tcx: TyCtxt<'_>, item: MonoItem, sym_name: String) -> (Mon
MonoItemKind::MonoItemFn {
name: name.clone(),
id,
body,
body: body.map(Phased::new),
},
details,
),
Expand Down Expand Up @@ -118,7 +120,7 @@ pub(super) fn mk_item(tcx: TyCtxt<'_>, item: MonoItem, sym_name: String) -> (Mon
name: static_def.name(),
id: static_def.def_id(),
allocation: alloc,
body,
body: body.map(Phased::new),
},
get_item_details(tcx, internal_id, None, None),
),
Expand Down
7 changes: 5 additions & 2 deletions src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
//! | Module | Responsibility |
//! |--------|----------------|
//! | [`schema`] | Data model types ([`SmirJson`], [`Item`], [`AllocInfo`], etc.) and type aliases; [`Item`] deliberately excludes `MonoItem` for structural phase separation |
//! | [`collect`] | Three-phase pipeline: collect items, analyze bodies, assemble final output; phase boundary is enforced structurally via the `(MonoItem, Item)` split |
//! | [`items`] | Constructing `(MonoItem, Item)` pairs and extracting debug-level details |
//! | [`collect`] | Three-phase pipeline: collect items, analyze bodies, assemble final output; phase boundary is enforced structurally via the `(MonoItem, Item)` split and [`phased::Phased`] body wrapper |
//! | [`items`] | Constructing `(MonoItem, Item<Raw>)` pairs and extracting debug-level details |
//! | [`phased`] | [`Phased<T, S>`](phased::Phased) typestate wrapper encoding the post-monomorphization body invariant |
//! | [`mir_visitor`] | `BodyAnalyzer`: single-pass MIR body traversal collecting calls, allocs, types, spans |
//! | [`ty_visitor`] | `TyCollector`: recursively collects reachable types with layout info (some special kinds are traversed but not stored) |
//! | [`link_map`] | Function resolution map: type + instance kind to symbol name |
Expand Down Expand Up @@ -49,6 +50,7 @@ mod collect;
mod items;
mod link_map;
mod mir_visitor;
mod phased;
mod schema;
mod ty_visitor;
mod types;
Expand All @@ -57,6 +59,7 @@ mod util;
// 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};
Comment on lines 59 to 63
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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>;

Copilot uses AI. Check for mistakes.
pub(crate) use util::hash;

Expand Down
103 changes: 103 additions & 0 deletions src/printer/phased.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Typestate wrapper encoding the post-monomorphization body invariant.
//!
//! After the collection pipeline's fixpoint loop resolves all unevaluated
//! constants, bodies stored in [`Item`](super::schema::Item) should contain
//! no `ConstantKind::Unevaluated`. [`Phased<T, S>`] encodes this guarantee
//! in the type system: phase 1+2 code works with `Phased<Body, Raw>`, and
//! the phase 2 -> 3 boundary validates each body (via [`UnevaluatedChecker`])
//! before producing `Phased<Body, Mono>`.
//!
//! [`Phased`] serializes transparently (delegates to the inner `T`), so
//! JSON output is unchanged.

use std::marker::PhantomData;

use crate::compat::serde;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
use crate::compat::serde;

Copilot uses AI. Check for mistakes.
use crate::compat::stable_mir;

use serde::{Serialize, Serializer};
use stable_mir::mir::visit::MirVisitor;
use stable_mir::mir::Body;

/// Pre-validation phase marker. Bodies may still contain `ConstantKind::Unevaluated`.
pub struct Raw;

/// Post-validation phase marker. Bodies are guaranteed free of `ConstantKind::Unevaluated`.
pub struct Mono;

/// A value of type `T` tagged with a phase marker `S`.
///
/// Construction of `Phased<T, Raw>` is unrestricted; `Phased<T, Mono>` can
/// only be obtained through [`Phased::<Body, Raw>::validate`], which walks
/// the body and panics if any unevaluated constant is found.
pub struct Phased<T, S>(T, PhantomData<S>);

impl<T, S> Phased<T, S> {
/// Borrow the inner value.
pub fn inner(&self) -> &T {
&self.0
}

/// Consume the wrapper, returning the inner value.
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
/// Consume the wrapper, returning the inner value.
/// Consume the wrapper, returning the inner value.
#[allow(dead_code)]

Copilot uses AI. Check for mistakes.
pub fn into_inner(self) -> T {
self.0
}
}

impl<T: Clone, S> Clone for Phased<T, S> {
fn clone(&self) -> Self {
Phased(self.0.clone(), PhantomData)
}
}

impl<T: Serialize, S> Serialize for Phased<T, S> {
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
self.0.serialize(serializer)
}
}

impl Phased<Body, Raw> {
/// Wrap a body in the `Raw` phase. Unrestricted.
pub fn new(body: Body) -> Self {
Phased(body, PhantomData)
}

/// 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);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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(...)).

Suggested change
UnevaluatedChecker.visit_body(&self.0);
let mut checker = UnevaluatedChecker;
checker.visit_body(&self.0);

Copilot uses AI. Check for mistakes.
Phased(self.0, PhantomData)
}
}

/// MIR visitor that panics if any `ConstantKind::Unevaluated` is encountered.
///
/// Used by [`Phased::<Body, Raw>::validate`] to enforce the post-mono invariant
/// at the phase 2 -> 3 boundary.
struct UnevaluatedChecker;

impl MirVisitor for UnevaluatedChecker {
fn visit_mir_const(
&mut self,
constant: &stable_mir::ty::MirConst,
loc: stable_mir::mir::visit::Location,
) {
if let stable_mir::ty::ConstantKind::Unevaluated(_) = constant.kind() {
panic!(
"Phased<Body, Raw>::validate: body contains ConstantKind::Unevaluated \
at location {loc:?}.\n\n\
The fixpoint loop in collect_and_analyze_items should have resolved \
all unevaluated constants before phase 3, but this one survived. \
The input program likely triggers a code path we haven't encountered yet.\n\n\
Action: please file an issue at \
https://github.com/runtimeverification/stable-mir-json/issues \
with the .rs file that triggered this panic so we can add it as a \
test case and extend the fixpoint loop to handle it."
);
}
self.super_mir_const(constant, loc);
}
}
Loading