use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct IrGenCheck {
pub description: String,
pub pattern: String,
pub required: bool,
pub min_count: usize,
pub exact: bool,
pub category: IrCheckCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IrCheckCategory {
MemoryAccess,
ControlFlow,
TypeLowering,
Constants,
DebugInfo,
Attributes,
CallingConv,
Globals,
AggregateAccess,
}
impl IrCheckCategory {
pub fn name(&self) -> &'static str {
match self {
Self::MemoryAccess => "memory-access",
Self::ControlFlow => "control-flow",
Self::TypeLowering => "type-lowering",
Self::Constants => "constants",
Self::DebugInfo => "debug-info",
Self::Attributes => "attributes",
Self::CallingConv => "calling-conv",
Self::Globals => "globals",
Self::AggregateAccess => "aggregate-access",
}
}
}
impl IrGenCheck {
pub fn required(description: &str, pattern: &str, category: IrCheckCategory) -> Self {
Self {
description: description.to_string(),
pattern: pattern.to_string(),
required: true,
min_count: 1,
exact: false,
category,
}
}
pub fn optional(description: &str, pattern: &str, category: IrCheckCategory) -> Self {
Self {
description: description.to_string(),
pattern: pattern.to_string(),
required: false,
min_count: 0,
exact: false,
category,
}
}
pub fn required_exact(description: &str, pattern: &str, category: IrCheckCategory) -> Self {
Self {
description: description.to_string(),
pattern: pattern.to_string(),
required: true,
min_count: 1,
exact: true,
category,
}
}
pub fn check(&self, ir: &str) -> bool {
let count = if self.exact {
ir.lines()
.filter(|l| l.trim() == self.pattern.trim())
.count()
} else {
ir.matches(&self.pattern).count()
};
if self.required {
count >= self.min_count
} else {
true
}
}
pub fn count_in(&self, ir: &str) -> usize {
if self.exact {
ir.lines()
.filter(|l| l.trim() == self.pattern.trim())
.count()
} else {
ir.matches(&self.pattern).count()
}
}
}
#[derive(Debug, Clone)]
pub struct IrGenTestResult {
pub name: String,
pub passed: bool,
pub ir_text: String,
pub check_results: Vec<IrCheckResult>,
}
#[derive(Debug, Clone)]
pub struct IrCheckResult {
pub description: String,
pub passed: bool,
pub count: usize,
pub category: IrCheckCategory,
}
impl IrGenTestResult {
pub fn from_checks(name: &str, ir_text: &str, checks: &[IrGenCheck]) -> Self {
let mut results = Vec::new();
let mut all_passed = true;
for check in checks {
let passed = check.check(ir_text);
let count = check.count_in(ir_text);
if !passed {
all_passed = false;
}
results.push(IrCheckResult {
description: check.description.clone(),
passed,
count,
category: check.category,
});
}
Self {
name: name.to_string(),
passed: all_passed,
ir_text: ir_text.to_string(),
check_results: results,
}
}
pub fn summary(&self) -> String {
let passed = self.check_results.iter().filter(|r| r.passed).count();
let total = self.check_results.len();
format!("{}: {}/{} checks passed", self.name, passed, total)
}
pub fn failed_checks(&self) -> Vec<&IrCheckResult> {
self.check_results.iter().filter(|r| !r.passed).collect()
}
}
pub fn ir_local_var_source() -> &'static str {
"int main() { int x = 42; return x; }"
}
pub fn ir_local_var_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"%x = alloca i32",
"alloca for local variable",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required(
"store i32 42",
"store constant to local",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required("load i32", "load from local", IrCheckCategory::MemoryAccess),
IrGenCheck::required("ret i32", "return i32 value", IrCheckCategory::ControlFlow),
]
}
pub fn ir_struct_gep_source() -> &'static str {
r#"
struct Point { int x; int y; };
int main() { struct Point p = {10, 20}; return p.x + p.y; }
"#
}
pub fn ir_struct_gep_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"alloca %struct.Point",
"alloca for struct",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required(
"getelementptr",
"GEP for struct field access",
IrCheckCategory::AggregateAccess,
),
IrGenCheck::required(
"store i32 10",
"store first field",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required(
"store i32 20",
"store second field",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required("add", "addition for x+y", IrCheckCategory::ControlFlow),
]
}
pub fn ir_array_gep_source() -> &'static str {
"int main() { int arr[3] = {1, 2, 3}; return arr[1]; }"
}
pub fn ir_array_gep_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"alloca [3 x i32]",
"alloca for array",
IrCheckCategory::MemoryAccess,
),
IrGenCheck::required(
"getelementptr",
"GEP for array indexing",
IrCheckCategory::AggregateAccess,
),
IrGenCheck::required(
"load i32",
"load from array element",
IrCheckCategory::MemoryAccess,
),
]
}
pub fn ir_if_else_source() -> &'static str {
"int max(int a, int b) { if (a > b) return a; else return b; }"
}
pub fn ir_if_else_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required("icmp", "integer comparison", IrCheckCategory::ControlFlow),
IrGenCheck::required("br i1", "conditional branch", IrCheckCategory::ControlFlow),
IrGenCheck::required(
"ret i32",
"return instruction",
IrCheckCategory::ControlFlow,
),
]
}
pub fn ir_for_loop_source() -> &'static str {
"int sum(int n) { int s = 0; for (int i = 0; i < n; i++) { s += i; } return s; }"
}
pub fn ir_for_loop_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"phi i32",
"phi node for induction variable",
IrCheckCategory::ControlFlow,
),
IrGenCheck::required("br", "branch for loop", IrCheckCategory::ControlFlow),
IrGenCheck::required(
"icmp",
"comparison for loop condition",
IrCheckCategory::ControlFlow,
),
IrGenCheck::required(
"add",
"addition for sum accumulation",
IrCheckCategory::ControlFlow,
),
]
}
pub fn ir_switch_source() -> &'static str {
"int f(int x) { switch (x) { case 0: return 1; case 1: return 2; default: return 0; } }"
}
pub fn ir_switch_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"switch i32",
"switch instruction",
IrCheckCategory::ControlFlow,
),
IrGenCheck::required("i32 0, label", "case 0 label", IrCheckCategory::ControlFlow),
IrGenCheck::required("i32 1, label", "case 1 label", IrCheckCategory::ControlFlow),
]
}
pub fn ir_function_call_source() -> &'static str {
r#"
int add(int a, int b) { return a + b; }
int main() { return add(3, 4); }
"#
}
pub fn ir_function_call_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"call i32 @add",
"function call",
IrCheckCategory::ControlFlow,
),
IrGenCheck::required(
"define i32 @add",
"function definition",
IrCheckCategory::ControlFlow,
),
]
}
pub fn ir_type_lowering_source() -> &'static str {
r#"
int main() {
int i = 1;
double d = 3.14;
char c = 'A';
long l = 123L;
float f = 1.5f;
int *p = &i;
return i + (int)d + (int)c;
}
"#
}
pub fn ir_type_lowering_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"alloca i32",
"int → i32 lowering",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required(
"alloca double",
"double → double lowering",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required(
"alloca i8",
"char → i8 lowering",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required(
"alloca i64",
"long → i64 lowering",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required(
"alloca float",
"float → float lowering",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required("alloca ptr", "pointer type", IrCheckCategory::TypeLowering),
IrGenCheck::required(
"sitofp",
"int to float conversion",
IrCheckCategory::TypeLowering,
),
IrGenCheck::required(
"fptosi",
"float to int conversion",
IrCheckCategory::TypeLowering,
),
]
}
pub fn ir_const_fold_source() -> &'static str {
"int main() { return 1 + 2 * 3 - 4 / 2; }"
}
pub fn ir_const_fold_checks() -> Vec<IrGenCheck> {
vec![IrGenCheck::required(
"ret i32 5",
"constant folded to 5",
IrCheckCategory::Constants,
)]
}
pub fn ir_global_const_source() -> &'static str {
r#"
const int MAX = 100;
int main() { return MAX; }
"#
}
pub fn ir_global_const_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"@MAX =",
"global constant declaration",
IrCheckCategory::Constants,
),
IrGenCheck::optional("constant", "constant qualifier", IrCheckCategory::Constants),
]
}
pub fn ir_debug_info_source() -> &'static str {
"int main(int argc, char **argv) { int x = 42; return x; }"
}
pub fn ir_debug_info_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"!dbg",
"debug location metadata",
IrCheckCategory::DebugInfo,
),
IrGenCheck::required(
"llvm.dbg.declare",
"debug declare intrinsic",
IrCheckCategory::DebugInfo,
),
IrGenCheck::optional(
"llvm.dbg.value",
"debug value intrinsic",
IrCheckCategory::DebugInfo,
),
IrGenCheck::required("!DI", "debug info metadata", IrCheckCategory::DebugInfo),
]
}
pub fn ir_attrs_source() -> &'static str {
r#"
__attribute__((noinline)) int foo(int x) { return x * 2; }
__attribute__((always_inline)) int bar(int x) { return x + 1; }
int main() { return foo(1) + bar(2); }
"#
}
pub fn ir_attrs_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"noinline",
"noinline attribute",
IrCheckCategory::Attributes,
),
IrGenCheck::required(
"alwaysinline",
"alwaysinline attribute",
IrCheckCategory::Attributes,
),
IrGenCheck::optional("optnone", "optnone attribute", IrCheckCategory::Attributes),
IrGenCheck::optional(
"ssp",
"stack protector attribute",
IrCheckCategory::Attributes,
),
]
}
pub fn ir_optnone_source() -> &'static str {
"__attribute__((optnone)) int slow() { return 0; }"
}
pub fn ir_optnone_checks() -> Vec<IrGenCheck> {
vec![IrGenCheck::required(
"optnone",
"optnone attribute present",
IrCheckCategory::Attributes,
)]
}
pub fn ir_cc_source() -> &'static str {
r#"
int cdecl_func(int a, int b) { return a + b; }
int __attribute__((fastcall)) fast_func(int a, int b) { return a * b; }
int main() { return cdecl_func(1, 2) + fast_func(3, 4); }
"#
}
pub fn ir_cc_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required(
"define",
"function definition",
IrCheckCategory::CallingConv,
),
IrGenCheck::required("call", "function call", IrCheckCategory::CallingConv),
IrGenCheck::optional(
"fastcall",
"fastcall convention",
IrCheckCategory::CallingConv,
),
]
}
pub fn ir_globals_source() -> &'static str {
r#"
int global_int = 42;
const int global_const = 100;
static int static_int = 0;
int uninitialized;
extern int external_var;
int main() { return global_int; }
"#
}
pub fn ir_globals_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required("@global_int =", "global variable", IrCheckCategory::Globals),
IrGenCheck::required(
"@global_const =",
"const global variable",
IrCheckCategory::Globals,
),
IrGenCheck::required(
"@static_int =",
"static global variable",
IrCheckCategory::Globals,
),
IrGenCheck::required(
"@uninitialized =",
"uninitialized global",
IrCheckCategory::Globals,
),
IrGenCheck::optional(
"@external_var =",
"external variable",
IrCheckCategory::Globals,
),
IrGenCheck::required("dso_local", "dso_local linkage", IrCheckCategory::Globals),
]
}
pub fn ir_global_init_source() -> &'static str {
"int arr[3] = {1, 2, 3}; int main() { return arr[0]; }"
}
pub fn ir_global_init_checks() -> Vec<IrGenCheck> {
vec![
IrGenCheck::required("@arr =", "global array", IrCheckCategory::Globals),
IrGenCheck::required(
"i32 1, i32 2, i32 3",
"array initializer",
IrCheckCategory::Globals,
),
]
}
#[derive(Debug, Clone)]
pub struct IrGenTestCase {
pub name: String,
pub source: String,
pub checks: Vec<IrGenCheck>,
pub category: IrCheckCategory,
}
impl IrGenTestCase {
pub fn new(
name: &str,
source: &str,
checks: Vec<IrGenCheck>,
category: IrCheckCategory,
) -> Self {
Self {
name: name.to_string(),
source: source.to_string(),
checks,
category,
}
}
}
pub fn build_ir_gen_test_suite() -> Vec<IrGenTestCase> {
vec![
IrGenTestCase::new(
"local_variable",
ir_local_var_source(),
ir_local_var_checks(),
IrCheckCategory::MemoryAccess,
),
IrGenTestCase::new(
"struct_gep",
ir_struct_gep_source(),
ir_struct_gep_checks(),
IrCheckCategory::AggregateAccess,
),
IrGenTestCase::new(
"array_gep",
ir_array_gep_source(),
ir_array_gep_checks(),
IrCheckCategory::AggregateAccess,
),
IrGenTestCase::new(
"if_else",
ir_if_else_source(),
ir_if_else_checks(),
IrCheckCategory::ControlFlow,
),
IrGenTestCase::new(
"for_loop",
ir_for_loop_source(),
ir_for_loop_checks(),
IrCheckCategory::ControlFlow,
),
IrGenTestCase::new(
"switch",
ir_switch_source(),
ir_switch_checks(),
IrCheckCategory::ControlFlow,
),
IrGenTestCase::new(
"function_call",
ir_function_call_source(),
ir_function_call_checks(),
IrCheckCategory::ControlFlow,
),
IrGenTestCase::new(
"type_lowering",
ir_type_lowering_source(),
ir_type_lowering_checks(),
IrCheckCategory::TypeLowering,
),
IrGenTestCase::new(
"const_fold",
ir_const_fold_source(),
ir_const_fold_checks(),
IrCheckCategory::Constants,
),
IrGenTestCase::new(
"global_const",
ir_global_const_source(),
ir_global_const_checks(),
IrCheckCategory::Constants,
),
IrGenTestCase::new(
"debug_info",
ir_debug_info_source(),
ir_debug_info_checks(),
IrCheckCategory::DebugInfo,
),
IrGenTestCase::new(
"function_attrs",
ir_attrs_source(),
ir_attrs_checks(),
IrCheckCategory::Attributes,
),
IrGenTestCase::new(
"optnone",
ir_optnone_source(),
ir_optnone_checks(),
IrCheckCategory::Attributes,
),
IrGenTestCase::new(
"calling_conv",
ir_cc_source(),
ir_cc_checks(),
IrCheckCategory::CallingConv,
),
IrGenTestCase::new(
"globals",
ir_globals_source(),
ir_globals_checks(),
IrCheckCategory::Globals,
),
IrGenTestCase::new(
"global_init",
ir_global_init_source(),
ir_global_init_checks(),
IrCheckCategory::Globals,
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ir_gen_check_required() {
let check = IrGenCheck::required("alloc test", "alloca", IrCheckCategory::MemoryAccess);
assert!(check.required);
assert_eq!(check.min_count, 1);
}
#[test]
fn test_ir_gen_check_optional() {
let check = IrGenCheck::optional("opt test", "phi", IrCheckCategory::ControlFlow);
assert!(!check.required);
}
#[test]
fn test_ir_gen_check_present() {
let check = IrGenCheck::required("test", "alloca", IrCheckCategory::MemoryAccess);
let ir = "alloca i32\nstore i32 0, i32* %1\nret i32 0";
assert!(check.check(ir));
}
#[test]
fn test_ir_gen_check_absent() {
let check = IrGenCheck::required("test", "fadd", IrCheckCategory::TypeLowering);
let ir = "alloca i32\nret i32 0";
assert!(!check.check(ir));
}
#[test]
fn test_ir_gen_check_count() {
let check = IrGenCheck::required("test", "alloca", IrCheckCategory::MemoryAccess);
let ir = "alloca i32\nalloca i64\nalloca double";
assert_eq!(check.count_in(ir), 3);
}
#[test]
fn test_ir_gen_check_exact() {
let check = IrGenCheck::required_exact("exact", "ret i32 0", IrCheckCategory::ControlFlow);
let ir = " ret i32 0\n ret i32 1";
assert!(!check.check(ir)); let ir2 = "ret i32 0";
assert!(check.check(ir2));
}
#[test]
fn test_ir_check_category_name() {
assert_eq!(IrCheckCategory::MemoryAccess.name(), "memory-access");
assert_eq!(IrCheckCategory::ControlFlow.name(), "control-flow");
assert_eq!(IrCheckCategory::DebugInfo.name(), "debug-info");
assert_eq!(IrCheckCategory::Globals.name(), "globals");
}
#[test]
fn test_ir_gen_test_result_all_pass() {
let checks = vec![
IrGenCheck::required("alloc", "alloca", IrCheckCategory::MemoryAccess),
IrGenCheck::required("ret", "ret", IrCheckCategory::ControlFlow),
];
let ir = "alloca i32\nret i32 0";
let result = IrGenTestResult::from_checks("test", ir, &checks);
assert!(result.passed);
}
#[test]
fn test_ir_gen_test_result_some_fail() {
let checks = vec![
IrGenCheck::required("alloc", "alloca", IrCheckCategory::MemoryAccess),
IrGenCheck::required("fadd", "fadd", IrCheckCategory::TypeLowering),
];
let ir = "alloca i32\nret i32 0";
let result = IrGenTestResult::from_checks("test", ir, &checks);
assert!(!result.passed);
assert_eq!(result.failed_checks().len(), 1);
}
#[test]
fn test_ir_gen_test_result_summary() {
let checks = vec![IrGenCheck::required(
"t1",
"alloca",
IrCheckCategory::MemoryAccess,
)];
let ir = "alloca i32";
let result = IrGenTestResult::from_checks("mytest", ir, &checks);
assert!(result.summary().contains("mytest"));
assert!(result.summary().contains("1/1"));
}
#[test]
fn test_ir_local_var_source() {
let src = ir_local_var_source();
assert!(src.contains("int x = 42"));
}
#[test]
fn test_ir_struct_gep_source() {
let src = ir_struct_gep_source();
assert!(src.contains("struct Point"));
assert!(src.contains("p.x"));
}
#[test]
fn test_ir_array_gep_source() {
let src = ir_array_gep_source();
assert!(src.contains("arr[3]"));
assert!(src.contains("arr[1]"));
}
#[test]
fn test_ir_if_else_source() {
let src = ir_if_else_source();
assert!(src.contains("a > b"));
}
#[test]
fn test_ir_for_loop_source() {
let src = ir_for_loop_source();
assert!(src.contains("for"));
assert!(src.contains("i++"));
}
#[test]
fn test_ir_switch_source() {
let src = ir_switch_source();
assert!(src.contains("switch"));
assert!(src.contains("case 0"));
}
#[test]
fn test_ir_function_call_source() {
let src = ir_function_call_source();
assert!(src.contains("add(3, 4)"));
}
#[test]
fn test_ir_type_lowering_source() {
let src = ir_type_lowering_source();
assert!(src.contains("double d"));
assert!(src.contains("char c"));
assert!(src.contains("long l"));
}
#[test]
fn test_ir_const_fold_source() {
let src = ir_const_fold_source();
assert!(src.contains("1 + 2 * 3 - 4 / 2"));
}
#[test]
fn test_ir_debug_info_source() {
let src = ir_debug_info_source();
assert!(src.contains("argc"));
assert!(src.contains("argv"));
}
#[test]
fn test_ir_attrs_source() {
let src = ir_attrs_source();
assert!(src.contains("noinline"));
assert!(src.contains("always_inline"));
}
#[test]
fn test_ir_local_var_checks_not_empty() {
assert!(!ir_local_var_checks().is_empty());
}
#[test]
fn test_ir_struct_gep_checks_has_gep() {
let checks = ir_struct_gep_checks();
assert!(checks.iter().any(|c| c.pattern.contains("getelementptr")));
}
#[test]
fn test_ir_for_loop_checks_has_phi() {
let checks = ir_for_loop_checks();
assert!(checks.iter().any(|c| c.pattern.contains("phi")));
}
#[test]
fn test_ir_type_lowering_checks_covers_types() {
let checks = ir_type_lowering_checks();
assert!(checks.iter().any(|c| c.pattern.contains("i32")));
assert!(checks.iter().any(|c| c.pattern.contains("double")));
assert!(checks.iter().any(|c| c.pattern.contains("i8")));
assert!(checks.iter().any(|c| c.pattern.contains("i64")));
}
#[test]
fn test_ir_debug_info_checks() {
let checks = ir_debug_info_checks();
assert!(checks.iter().any(|c| c.pattern.contains("!dbg")));
assert!(checks.iter().any(|c| c.pattern.contains("dbg.declare")));
}
#[test]
fn test_ir_attrs_checks() {
let checks = ir_attrs_checks();
assert!(checks.iter().any(|c| c.pattern.contains("noinline")));
assert!(checks.iter().any(|c| c.pattern.contains("alwaysinline")));
}
#[test]
fn test_build_ir_gen_test_suite_not_empty() {
let suite = build_ir_gen_test_suite();
assert!(!suite.is_empty());
}
#[test]
fn test_build_ir_gen_test_suite_covers_categories() {
let suite = build_ir_gen_test_suite();
let cats: Vec<IrCheckCategory> = suite.iter().map(|t| t.category).collect();
assert!(cats.contains(&IrCheckCategory::MemoryAccess));
assert!(cats.contains(&IrCheckCategory::ControlFlow));
assert!(cats.contains(&IrCheckCategory::TypeLowering));
assert!(cats.contains(&IrCheckCategory::Constants));
assert!(cats.contains(&IrCheckCategory::DebugInfo));
assert!(cats.contains(&IrCheckCategory::Attributes));
assert!(cats.contains(&IrCheckCategory::CallingConv));
assert!(cats.contains(&IrCheckCategory::Globals));
assert!(cats.contains(&IrCheckCategory::AggregateAccess));
}
#[test]
fn test_ir_gen_test_case_new() {
let tc = IrGenTestCase::new(
"test",
"int main() { return 0; }",
vec![],
IrCheckCategory::ControlFlow,
);
assert_eq!(tc.name, "test");
assert_eq!(tc.category, IrCheckCategory::ControlFlow);
}
#[test]
fn test_ir_gen_all_checks_valid() {
let suite = build_ir_gen_test_suite();
for test in &suite {
assert!(!test.name.is_empty());
assert!(!test.source.is_empty());
assert!(!test.checks.is_empty(), "test {} has no checks", test.name);
}
}
}