Skip to content

Commit 10f31cc

Browse files
committed
Initial version of this check
1 parent b49c7d7 commit 10f31cc

File tree

7 files changed

+206
-1
lines changed

7 files changed

+206
-1
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
2+
use rustc_data_structures::graph::scc::Sccs;
3+
use rustc_data_structures::graph::{DirectedGraph, Successors};
4+
use rustc_hir as hir;
5+
use rustc_index::{IndexVec, newtype_index};
6+
use rustc_middle::mir::interpret::{AllocId, GlobalAlloc};
7+
use rustc_middle::ty::TyCtxt;
8+
use rustc_span::ErrorGuaranteed;
9+
use rustc_span::def_id::LocalDefId;
10+
11+
// --- graph indices
12+
newtype_index! {
13+
struct StaticNodeIdx {}
14+
}
15+
newtype_index! {
16+
#[derive(Ord, PartialOrd)]
17+
struct StaticSccIdx {}
18+
}
19+
20+
// --- adjacency-list graph for rustc_data_structures::graph::scc
21+
struct StaticRefGraph {
22+
succ: IndexVec<StaticNodeIdx, Vec<StaticNodeIdx>>,
23+
}
24+
25+
impl DirectedGraph for StaticRefGraph {
26+
type Node = StaticNodeIdx;
27+
28+
fn num_nodes(&self) -> usize {
29+
self.succ.len()
30+
}
31+
}
32+
33+
impl Successors for StaticRefGraph {
34+
fn successors(&self, n: StaticNodeIdx) -> impl Iterator<Item = StaticNodeIdx> {
35+
self.succ[n].iter().copied()
36+
}
37+
}
38+
39+
pub(crate) fn check_static_initializer_acyclic(
40+
tcx: TyCtxt<'_>,
41+
_: (),
42+
) -> Result<(), ErrorGuaranteed> {
43+
// Collect local statics
44+
let mut statics: Vec<LocalDefId> = Vec::new();
45+
for item_id in tcx.hir_free_items() {
46+
let item = tcx.hir_item(item_id);
47+
if matches!(item.kind, hir::ItemKind::Static(..)) {
48+
statics.push(item.owner_id.def_id);
49+
}
50+
}
51+
52+
// Fast path
53+
if statics.is_empty() {
54+
return Ok(());
55+
}
56+
57+
// Map statics to dense node indices
58+
let mut node_of: FxHashMap<LocalDefId, StaticNodeIdx> = FxHashMap::default();
59+
let mut def_of: IndexVec<StaticNodeIdx, LocalDefId> = IndexVec::new();
60+
for &def_id in statics.iter() {
61+
let idx = def_of.push(def_id);
62+
node_of.insert(def_id, idx);
63+
}
64+
65+
let mut graph = StaticRefGraph { succ: IndexVec::from_elem_n(Vec::new(), def_of.len()) };
66+
67+
// Build edges by evaluating each static initializer and scanning provenance
68+
for &from_def in &statics {
69+
let from_node = node_of[&from_def];
70+
71+
// If const-eval already errored for this static, skip (we don't want noisy follow-ups).
72+
let Ok(root_alloc) = tcx.eval_static_initializer(from_def) else {
73+
continue;
74+
};
75+
76+
let mut out_edges: FxHashSet<StaticNodeIdx> = FxHashSet::default();
77+
collect_referenced_local_statics(tcx, root_alloc, &node_of, &mut out_edges);
78+
#[allow(rustc::potential_query_instability)]
79+
graph.succ[from_node].extend(out_edges);
80+
}
81+
82+
// 4) SCCs
83+
let sccs: Sccs<StaticNodeIdx, StaticSccIdx> = Sccs::new(&graph); // :contentReference[oaicite:4]{index=4}
84+
85+
// Group members by SCC
86+
let mut members: IndexVec<StaticSccIdx, Vec<StaticNodeIdx>> =
87+
IndexVec::from_elem_n(Vec::new(), sccs.num_sccs());
88+
89+
for i in 0..def_of.len() {
90+
let n = StaticNodeIdx::from_usize(i);
91+
members[sccs.scc(n)].push(n);
92+
}
93+
94+
// 5) Emit errors for cyclic SCCs
95+
let mut first_guar: Option<ErrorGuaranteed> = None;
96+
97+
for scc in sccs.all_sccs() {
98+
let nodes = &members[scc];
99+
if nodes.is_empty() {
100+
continue;
101+
}
102+
103+
let is_cycle = nodes.len() > 1
104+
|| (nodes.len() == 1 && graph.successors(nodes[0]).any(|x| x == nodes[0]));
105+
if !is_cycle {
106+
continue;
107+
}
108+
109+
let head_def = def_of[nodes[0]];
110+
let head_span = tcx.def_span(head_def);
111+
112+
let mut diag = tcx.dcx().struct_span_err(
113+
head_span,
114+
format!(
115+
"static initializer forms a cycle involving `{}`",
116+
tcx.def_path_str(head_def.to_def_id()),
117+
),
118+
);
119+
120+
for &n in nodes {
121+
let d = def_of[n];
122+
diag.span_label(tcx.def_span(d), "part of this cycle");
123+
}
124+
125+
diag.note(format!(
126+
"cyclic static initializer references are not supported for target `{}`",
127+
tcx.sess.target.llvm_target
128+
));
129+
130+
let guar = diag.emit();
131+
first_guar.get_or_insert(guar);
132+
}
133+
134+
match first_guar {
135+
Some(g) => Err(g),
136+
None => Ok(()),
137+
}
138+
}
139+
140+
// Traverse allocations reachable from the static initializer allocation and collect local-static targets.
141+
fn collect_referenced_local_statics<'tcx>(
142+
tcx: TyCtxt<'tcx>,
143+
root_alloc: rustc_middle::mir::interpret::ConstAllocation<'tcx>,
144+
node_of: &FxHashMap<LocalDefId, StaticNodeIdx>,
145+
out: &mut FxHashSet<StaticNodeIdx>,
146+
) {
147+
let mut stack: Vec<AllocId> = Vec::new();
148+
let mut seen: FxHashSet<AllocId> = FxHashSet::default();
149+
150+
// Scan the root allocation for pointers first.
151+
push_ptr_alloc_ids(root_alloc.inner(), &mut stack);
152+
153+
while let Some(alloc_id) = stack.pop() {
154+
if !seen.insert(alloc_id) {
155+
continue;
156+
}
157+
158+
match tcx.global_alloc(alloc_id) {
159+
GlobalAlloc::Static(def_id) => {
160+
if let Some(local_def) = def_id.as_local()
161+
&& let Some(&node) = node_of.get(&local_def)
162+
{
163+
out.insert(node);
164+
}
165+
}
166+
167+
GlobalAlloc::Memory(const_alloc) => {
168+
push_ptr_alloc_ids(const_alloc.inner(), &mut stack);
169+
}
170+
171+
_ => {
172+
// Functions, vtables, etc: ignore
173+
}
174+
}
175+
}
176+
}
177+
178+
// Extract all AllocIds referenced by pointers in this allocation via provenance.
179+
fn push_ptr_alloc_ids(alloc: &rustc_middle::mir::interpret::Allocation, stack: &mut Vec<AllocId>) {
180+
for (_, prov) in alloc.provenance().ptrs().iter() {
181+
stack.push(prov.alloc_id());
182+
}
183+
}

compiler/rustc_const_eval/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// tidy-alphabetical-end
1616

1717
pub mod check_consts;
18+
mod check_static_initializer_acyclic;
1819
pub mod const_eval;
1920
mod errors;
2021
pub mod interpret;
@@ -48,6 +49,8 @@ pub fn provide(providers: &mut Providers) {
4849
};
4950
providers.hooks.validate_scalar_in_layout =
5051
|tcx, scalar, layout| util::validate_scalar_in_layout(tcx, scalar, layout);
52+
providers.check_static_initializer_acyclic =
53+
check_static_initializer_acyclic::check_static_initializer_acyclic;
5154
}
5255

5356
/// `rustc_driver::main` installs a handler that will set this to `true` if

compiler/rustc_interface/src/passes.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,10 @@ fn run_required_analyses(tcx: TyCtxt<'_>) {
11331133
}
11341134
});
11351135
});
1136-
1136+
// NEW: target-gated pre-codegen error
1137+
if tcx.sess.target.options.requires_static_initializer_acyclic {
1138+
tcx.ensure_ok().check_static_initializer_acyclic(());
1139+
}
11371140
sess.time("layout_testing", || layout_test::test_layout(tcx));
11381141
sess.time("abi_testing", || abi_test::test_abi(tcx));
11391142
}

compiler/rustc_middle/src/query/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ rustc_queries! {
258258
desc { |tcx| "getting HIR parent of `{}`", tcx.def_path_str(key) }
259259
}
260260

261+
query check_static_initializer_acyclic(_: ()) -> Result<(), ErrorGuaranteed> {
262+
desc { "checking that static initializers are acyclic" }
263+
}
261264
/// Gives access to the HIR nodes and bodies inside `key` if it's a HIR owner.
262265
///
263266
/// This can be conveniently accessed by `tcx.hir_*` methods.

compiler/rustc_target/src/spec/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2394,6 +2394,9 @@ pub struct TargetOptions {
23942394
pub archive_format: StaticCow<str>,
23952395
/// Is asm!() allowed? Defaults to true.
23962396
pub allow_asm: bool,
2397+
/// Static initializers must be acyclic.
2398+
/// Defaults to false
2399+
pub requires_static_initializer_acyclic: bool,
23972400
/// Whether the runtime startup code requires the `main` function be passed
23982401
/// `argc` and `argv` values.
23992402
pub main_needs_argc_argv: bool,
@@ -2777,6 +2780,7 @@ impl Default for TargetOptions {
27772780
archive_format: "gnu".into(),
27782781
main_needs_argc_argv: true,
27792782
allow_asm: true,
2783+
requires_static_initializer_acyclic: false,
27802784
has_thread_local: false,
27812785
obj_is_bitcode: false,
27822786
min_atomic_width: None,

compiler/rustc_target/src/spec/targets/nvptx64_nvidia_cuda.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ pub(crate) fn target() -> Target {
5959
// Support using `self-contained` linkers like the llvm-bitcode-linker
6060
link_self_contained: LinkSelfContainedDefault::True,
6161

62+
// Static initializers must not have cycles on this target
63+
requires_static_initializer_acyclic: true,
64+
6265
..Default::default()
6366
},
6467
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//@ only-nvptx64
2+
//@ ignore-backends: gcc
3+
#![crate_type = "rlib"]
4+
#![no_std]
5+
struct Bar(&'static Bar);
6+
static FOO: Bar = Bar(&FOO); //~ ERROR static initializer forms a cycle involving `FOO`

0 commit comments

Comments
 (0)