use std::path::PathBuf;
use std::sync::Arc;
use rcc_cfg::{
build_bodies,
verify::{verify_body_with_hir, CfgErrorKind},
BinOp, Body, ConstKind, Operand, Projection, Rvalue, StatementKind,
};
use rcc_errors::{CaptureEmitter, Diagnostic, Handler};
use rcc_hir::{DefId, DefKind, GlobalInitValue, HirCrate, Local, ObjectQuals, Ty, TyCtxt};
use rcc_hir_lower::lower;
use rcc_session::{Options, Session};
use rcc_typeck::{check, verify_typed_hir};
struct Checked {
session: Session,
cap: CaptureEmitter,
tcx: TyCtxt,
hir: HirCrate,
}
struct Lowered {
tcx: TyCtxt,
hir: HirCrate,
bodies: Vec<(DefId, Body)>,
}
fn check_snippet(name: &str, src: &str) -> Checked {
check_snippet_with_options(name, src, Options::default())
}
fn check_snippet_with_options(name: &str, src: &str, opts: Options) -> Checked {
let cap = CaptureEmitter::new();
let handler = Handler::with_emitter(Box::new(cap.clone()));
let mut session = Session::with_handler(opts, handler);
let file = session
.source_map
.write()
.unwrap()
.add_file(PathBuf::from(format!("<pre-codegen/{name}>")), Arc::from(src));
let pp_tokens = rcc_preprocess::preprocess(&mut session, file);
let ast = rcc_parse::parse(&mut session, pp_tokens).expect("parse returned None");
let mut tcx = TyCtxt::new();
let mut hir = lower(&ast, &mut tcx, &mut session);
check(&mut session, &mut tcx, &mut hir);
verify_typed_hir(&mut session, &tcx, &hir);
Checked { session, cap, tcx, hir }
}
fn lower_checked(name: &str, src: &str) -> Lowered {
lower_checked_with_options(name, src, Options::default())
}
fn lower_checked_with_options(name: &str, src: &str, opts: Options) -> Lowered {
let mut checked = check_snippet_with_options(name, src, opts);
assert!(
!checked.session.handler.has_errors(),
"{name}: unexpected diagnostics: {:?}",
checked.cap.diagnostics()
);
let mut bodies: Vec<_> =
build_bodies(&mut checked.session, &checked.tcx, &checked.hir).into_iter().collect();
bodies.sort_by_key(|(def, _)| def.0);
for (_, body) in &bodies {
verify_body_with_hir(body, &checked.tcx, &checked.hir)
.unwrap_or_else(|errors| panic!("{name}: CFG verifier errors: {errors:?}"));
}
assert!(
!checked.session.handler.has_errors(),
"{name}: CFG build emitted diagnostics: {:?}",
checked.cap.diagnostics()
);
Lowered { tcx: checked.tcx, hir: checked.hir, bodies }
}
#[test]
fn overflow_builtins_reach_cfg_as_checked_overflow_rvalues() {
let src = r#"
int f(unsigned a, int b) {
int out;
return __builtin_add_overflow(a, b, &out)
+ __builtin_mul_overflow_p(a, b, 0);
}
"#;
let opts = Options { gnu_builtin_libcalls: true, ..Options::default() };
let lowered = lower_checked_with_options("overflow-builtins", src, opts);
let body = &lowered.bodies[0].1;
let mut saw_add_store = false;
let mut saw_mul_predicate = false;
for block in body.blocks.iter() {
for stmt in &block.statements {
let StatementKind::Assign { rvalue, .. } = &stmt.kind else { continue };
match rvalue {
Rvalue::CheckedOverflow { op: BinOp::Add, dst: Some(_), ty, .. } => {
assert_eq!(*ty, lowered.tcx.int);
saw_add_store = true;
}
Rvalue::CheckedOverflow { op: BinOp::Mul, dst: None, ty, .. } => {
assert_eq!(*ty, lowered.tcx.int);
saw_mul_predicate = true;
}
_ => {}
}
}
}
assert!(saw_add_store, "expected add overflow with result pointer store");
assert!(saw_mul_predicate, "expected mul overflow predicate without result pointer");
}
fn expect_errors(name: &str, src: &str, expected_codes: &[&'static str]) -> Vec<Diagnostic> {
let checked = check_snippet(name, src);
let diagnostics = checked.cap.diagnostics();
assert!(
checked.session.handler.has_errors(),
"{name}: expected diagnostics before CFG/codegen"
);
for code in expected_codes {
assert!(
diagnostics.iter().any(|d| d.code == Some(*code)),
"{name}: expected diagnostic {code}, got {diagnostics:?}"
);
}
diagnostics
}
fn expect_warnings_and_lower(name: &str, src: &str, expected_codes: &[&'static str]) -> Lowered {
let mut checked = check_snippet(name, src);
let diagnostics = checked.cap.diagnostics();
assert!(
!checked.session.handler.has_errors(),
"{name}: warnings should not stop CFG/codegen: {diagnostics:?}"
);
for code in expected_codes {
assert!(
diagnostics.iter().any(|d| d.code == Some(*code)),
"{name}: expected warning {code}, got {diagnostics:?}"
);
}
let mut bodies: Vec<_> =
build_bodies(&mut checked.session, &checked.tcx, &checked.hir).into_iter().collect();
bodies.sort_by_key(|(def, _)| def.0);
for (_, body) in &bodies {
verify_body_with_hir(body, &checked.tcx, &checked.hir)
.unwrap_or_else(|errors| panic!("{name}: CFG verifier errors: {errors:?}"));
}
Lowered { tcx: checked.tcx, hir: checked.hir, bodies }
}
#[test]
fn member_access_field_index_survives_to_cfg() {
let lowered = lower_checked(
"member-second-field",
"struct S { int a; int b; }; int f(struct S s) { return s.b; }",
);
let body = only_body(&lowered);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Copy(src) | Operand::Move(src)),
} if place.base.0 == 0
&& src.projection.iter().any(|p| matches!(p, Projection::Field(1)))
)
}),
"return s.b should store from Projection::Field(1)"
);
}
#[test]
fn return_coercion_is_explicit_before_codegen() {
let lowered = lower_checked("return-coercion", "long f(int x) { return x; }");
let body = only_body(&lowered);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
rvalue: Rvalue::Cast { to, .. },
..
} if *to == lowered.tcx.long
)
}),
"int-to-long return coercion should be an explicit Cast rvalue"
);
}
#[test]
fn folded_global_initializer_is_ready_for_codegen() {
let lowered = lower_checked("global-init-fold", "static int x = 2 + 3;");
let init = lowered.hir.defs.iter().find_map(|def| match &def.kind {
DefKind::Global { init: Some(init), .. } => Some(init),
_ => None,
});
let init = init.expect("expected one global initializer");
assert_eq!(init.entries.len(), 1);
assert!(matches!(init.entries[0].value, GlobalInitValue::Int(5)));
}
#[test]
fn global_object_read_lowers_to_explicit_load() {
let lowered =
lower_checked("global-object-read", "static int x = 5; int f(void) { return x; }");
let body = only_body(&lowered);
let x = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
rvalue: Rvalue::LoadGlobal { def, ty },
..
} if *def == x && *ty == lowered.tcx.int
)
}),
"return x should load the object value from global x"
);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).all(|stmt| {
!matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Const(c)),
} if place.base.0 == 0 && matches!(c.kind, ConstKind::Global(def) if def == x)
)
}),
"return x must not store the address constant global#x into the return slot"
);
}
#[test]
fn gnu_lvalue_comma_global_rhs_lowers_as_object_value() {
let lowered = expect_warnings_and_lower(
"gnu-lvalue-comma-global-rhs",
"volatile int g2; int g3; void f(void) { volatile int *p = &g2; *p = (g3, g2); }",
&["W0021"],
);
let body = only_body(&lowered);
let g2 = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
rvalue: Rvalue::Use(Operand::Copy(src) | Operand::Move(src)),
..
} if matches!(src.projection.as_slice(), [Projection::Global(def)] if *def == g2)
)
}),
"the rhs of a GNU lvalue comma should be copied from global object storage"
);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).all(|stmt| {
!matches!(
&stmt.kind,
StatementKind::Assign {
rvalue: Rvalue::Use(Operand::Const(c)),
..
} if c.ty == lowered.tcx.int
&& matches!(c.kind, ConstKind::Global(def) if def == g2)
)
}),
"the global object rhs must not lower as an int-typed address constant"
);
}
#[test]
fn logical_and_with_constant_true_normalizes_short_lhs_to_int() {
let lowered = lower_checked(
"logical-and-constant-true-normalization",
"int g; void f(void) { short s = 7; g = (s < (s && 4294967293UL)); }",
);
let body = only_body(&lowered);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
let StatementKind::Assign { rvalue: Rvalue::BinaryOp(BinOp::SLt, lhs, rhs), .. } =
&stmt.kind
else {
return false;
};
operand_direct_ty(body, lhs) == Some(lowered.tcx.int)
&& operand_direct_ty(body, rhs) == Some(lowered.tcx.int)
}),
"`s && constant-true` should still materialize an int 0/1 before comparison"
);
}
#[test]
fn global_object_address_stays_address_value() {
let lowered = lower_checked("global-address", "static int x; int *f(void) { return &x; }");
let body = only_body(&lowered);
let x = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Const(c)),
} if place.base.0 == 0 && matches!(c.kind, ConstKind::Global(def) if def == x)
)
}),
"return &x should pass the global address constant through to the pointer return slot"
);
}
#[test]
fn global_object_assignment_lowers_to_global_place() {
let lowered = lower_checked("global-object-assign", "int x; int f(void) { x = 7; return x; }");
let body = only_body(&lowered);
let x = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Const(c)),
} if matches!(place.projection.as_slice(), [Projection::Global(def)] if *def == x)
&& matches!(c.kind, ConstKind::Int(7))
)
}),
"x = 7 should store through a global place, not panic in lower_as_place"
);
}
#[test]
fn global_object_field_assignment_lowers_to_projected_global_place() {
let lowered = lower_checked(
"global-object-field-assign",
"typedef struct { int x; int y; } S; S v; int f(void) { v.x = 1; return v.x; }",
);
let body = only_body(&lowered);
let v = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Const(c)),
} if matches!(
place.projection.as_slice(),
[Projection::Global(def), Projection::Field(0)] if *def == v
) && matches!(c.kind, ConstKind::Int(1))
)
}),
"v.x = 1 should store through a projected global place"
);
}
#[test]
fn deref_of_global_address_loads_object_value() {
let lowered =
lower_checked("global-deref-address", "static int x = 3; int f(void) { return *&x; }");
let body = only_body(&lowered);
let x = find_global(&lowered.hir);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
rvalue: Rvalue::Use(Operand::Const(c)),
..
} if matches!(c.kind, ConstKind::Global(def) if def == x)
)
}),
"*&x should materialize the global address"
);
assert!(
body.blocks.iter().flat_map(|block| &block.statements).any(|stmt| {
matches!(
&stmt.kind,
StatementKind::Assign {
place,
rvalue: Rvalue::Use(Operand::Copy(src) | Operand::Move(src)),
} if place.base.0 == 0
&& src.projection.iter().any(|p| matches!(p, Projection::Deref))
)
}),
"return *&x should copy from a dereferenced address place into the return slot"
);
}
#[test]
fn function_designator_return_is_not_global_object_load() {
let lowered = lower_checked(
"function-designator-return",
"int f(void) { return 1; } int (*g(void))(void) { return f; }",
);
let g_body = lowered
.bodies
.iter()
.find(|(_, body)| body.ret_ty.is_some_and(|ret| matches!(lowered.tcx.get(ret), Ty::Ptr(_))))
.map(|(_, body)| body)
.expect("expected function returning a function pointer");
assert!(
g_body.blocks.iter().flat_map(|block| &block.statements).all(|stmt| {
!matches!(&stmt.kind, StatementKind::Assign { rvalue: Rvalue::LoadGlobal { .. }, .. })
}),
"returning function designator f must not become a global object load"
);
}
#[test]
fn verifier_rejects_global_address_stored_as_scalar() {
let lowered =
lower_checked("global-address-old-shape", "static int x = 5; int f(void) { return x; }");
let mut body = only_body(&lowered).clone();
let x = find_global(&lowered.hir);
body.blocks[rcc_cfg::BasicBlockId(0)].statements.insert(
0,
rcc_cfg::Statement {
kind: StatementKind::Assign {
place: rcc_cfg::Place { base: Local(0), projection: Vec::new() },
rvalue: Rvalue::Use(Operand::Const(rcc_cfg::Const {
kind: ConstKind::Global(x),
ty: lowered.tcx.int,
})),
},
span: rcc_span::DUMMY_SP,
},
);
let errors = verify_body_with_hir(&body, &lowered.tcx, &lowered.hir).unwrap_err();
assert!(errors.iter().any(|err| matches!(
err.kind,
CfgErrorKind::InvalidGlobalAddressType { def, ty }
if def == x && ty == lowered.tcx.int
)));
}
#[test]
fn volatile_local_metadata_survives_to_cfg() {
let lowered = lower_checked(
"volatile-metadata",
"int f(void) { volatile int x; int y = x; x = y; return y; }",
);
let body = only_body(&lowered);
assert!(
body.locals.iter().any(|decl| decl.quals == volatile_quals()),
"volatile local qualifier should reach CFG LocalDecl metadata"
);
}
#[test]
fn c11_alignas_local_metadata_survives_to_cfg() {
let opts =
Options { language_standard: rcc_session::LanguageStandard::C11, ..Options::default() };
let lowered =
lower_checked_with_options("alignas-metadata", "int f(void) { _Alignas(16) int x; }", opts);
let body = only_body(&lowered);
assert!(
body.locals.iter().any(|decl| decl.align_override == Some(16)),
"C11 `_Alignas` should reach CFG LocalDecl metadata"
);
}
#[test]
fn gnu_vector_initializer_reaches_cfg_as_vector_rvalue() {
let src = r#"
typedef int v4si __attribute__((vector_size(16)));
int main(void) {
v4si x = { 1, 2, 3, 4 };
return 0;
}
"#;
let opts = Options { gnu_attributes: true, ..Options::default() };
let lowered = lower_checked_with_options("gnu-vector-init", src, opts);
let vector_ty = lowered
.hir
.defs
.iter()
.find_map(|def| match def.kind {
DefKind::Typedef(ty) if matches!(lowered.tcx.get(ty), Ty::Vector { .. }) => Some(ty),
_ => None,
})
.expect("vector typedef");
let body = &lowered.bodies[0].1;
let mut vector_init_count = 0;
for block in body.blocks.iter() {
for stmt in &block.statements {
if let StatementKind::Assign { place, rvalue: Rvalue::VectorInit { ty, lanes } } =
&stmt.kind
{
assert_eq!(*ty, vector_ty);
assert_eq!(lanes.len(), 4);
assert!(
place.projection.is_empty(),
"vector initializer should assign a scalar-like vector place"
);
vector_init_count += 1;
}
}
}
assert_eq!(vector_init_count, 1, "expected one vector initializer rvalue");
}
#[test]
fn invalid_return_stops_before_cfg() {
let diagnostics = expect_errors(
"invalid-return",
"struct A { int x; }; struct B { int x; }; struct A f(struct B b) { return b; }",
&["E0081"],
);
assert!(diagnostics.iter().all(|d| d.code != Some("E0088")));
}
#[test]
fn incompatible_pointer_assignment_warns_and_reaches_cfg() {
let lowered = expect_warnings_and_lower(
"incompatible-pointer-assignment",
"int f(char *q) { int *p; p = q; return 0; }",
&["E0082"],
);
assert_eq!(lowered.bodies.len(), 1);
}
#[test]
fn incompatible_call_pointer_coercion_warns_and_reaches_cfg() {
let lowered = expect_warnings_and_lower(
"incompatible-call-coercion",
"int sink(int *p); int f(char *q) { return sink(q); }",
&["E0082"],
);
assert_eq!(lowered.bodies.len(), 1);
}
#[test]
fn const_assignment_stops_before_cfg() {
expect_errors(
"const-assignment",
"int f(void) { const int x = 0; x = 1; return x; }",
&["E0080"],
);
}
fn only_body(lowered: &Lowered) -> &Body {
assert_eq!(lowered.bodies.len(), 1);
&lowered.bodies[0].1
}
fn find_global(hir: &HirCrate) -> DefId {
hir.defs
.iter_enumerated()
.find_map(|(def, data)| matches!(data.kind, DefKind::Global { .. }).then_some(def))
.expect("missing global")
}
fn volatile_quals() -> ObjectQuals {
ObjectQuals { is_const: false, is_volatile: true, is_restrict: false }
}
fn operand_direct_ty(body: &Body, operand: &Operand) -> Option<rcc_hir::TyId> {
match operand {
Operand::Copy(place) | Operand::Move(place) if place.projection.is_empty() => {
body.locals.get(place.base).map(|decl| decl.ty)
}
Operand::Const(c) => Some(c.ty),
_ => None,
}
}