use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
use super::rules::{Diagnostic, DiagnosticSeverity, Range, Rule, RuleCode};
lazy_static! {
static ref FUEL_SETTING: Regex = Regex::new(r#"--fuel\s+(\d+)"#).unwrap();
static ref IFUEL_SETTING: Regex = Regex::new(r#"--ifuel\s+(\d+)"#).unwrap();
static ref RLIMIT_SETTING: Regex = Regex::new(r#"--z3rlimit\s+(\d+)"#).unwrap();
static ref QUAKE_OPTION: Regex = Regex::new(r"--quake").unwrap();
static ref RETRY_OPTION: Regex = Regex::new(r"--retry").unwrap();
static ref LONG_CALC: Regex = Regex::new(r"calc\s*\(\s*==\s*\)").unwrap();
static ref MANY_ASSERTS: Regex = Regex::new(r"assert\s*\(").unwrap();
static ref SMTPAT_MANY: Regex = Regex::new(r"SMTPat").unwrap();
static ref PUSH_OPTIONS: Regex = Regex::new(r"#push-options").unwrap();
static ref SPLIT_QUERIES: Regex = Regex::new(r"--split_queries").unwrap();
}
pub struct PerfProfilerRule {
pub fuel_threshold: u32,
pub ifuel_threshold: u32,
pub rlimit_threshold: u32,
pub assert_count_threshold: usize,
}
impl PerfProfilerRule {
pub fn new() -> Self {
Self {
fuel_threshold: 5,
ifuel_threshold: 3,
rlimit_threshold: 300,
assert_count_threshold: 50,
}
}
pub fn with_thresholds(
fuel_threshold: u32,
ifuel_threshold: u32,
rlimit_threshold: u32,
assert_count_threshold: usize,
) -> Self {
Self {
fuel_threshold,
ifuel_threshold,
rlimit_threshold,
assert_count_threshold,
}
}
fn check_fuel(&self, file: &PathBuf, line_num: usize, line: &str) -> Option<Diagnostic> {
if let Some(caps) = FUEL_SETTING.captures(line) {
if let Ok(fuel) = caps.get(1).unwrap().as_str().parse::<u32>() {
if fuel > self.fuel_threshold {
let severity = if fuel > self.fuel_threshold + 2 {
DiagnosticSeverity::Warning
} else {
DiagnosticSeverity::Info
};
return Some(Diagnostic {
rule: RuleCode::FST016,
severity,
file: file.clone(),
range: Range::point(line_num + 1, 1),
message: format!(
"High fuel setting ({}) may indicate proof complexity. \
Consider splitting into smaller lemmas or adding explicit hints.",
fuel
),
fix: None,
});
}
}
}
None
}
fn check_ifuel(&self, file: &PathBuf, line_num: usize, line: &str) -> Option<Diagnostic> {
if let Some(caps) = IFUEL_SETTING.captures(line) {
if let Ok(ifuel) = caps.get(1).unwrap().as_str().parse::<u32>() {
if ifuel > self.ifuel_threshold {
let severity = if ifuel > self.ifuel_threshold + 1 {
DiagnosticSeverity::Warning
} else {
DiagnosticSeverity::Info
};
return Some(Diagnostic {
rule: RuleCode::FST016,
severity,
file: file.clone(),
range: Range::point(line_num + 1, 1),
message: format!(
"High ifuel setting ({}) suggests deep inductive reasoning. \
Consider adding explicit inversion lemmas.",
ifuel
),
fix: None,
});
}
}
}
None
}
fn check_rlimit(&self, file: &PathBuf, line_num: usize, line: &str) -> Option<Diagnostic> {
if let Some(caps) = RLIMIT_SETTING.captures(line) {
if let Ok(rlimit) = caps.get(1).unwrap().as_str().parse::<u32>() {
if rlimit > self.rlimit_threshold {
let severity = if rlimit > self.rlimit_threshold * 3 {
DiagnosticSeverity::Error
} else if rlimit > self.rlimit_threshold * 2 {
DiagnosticSeverity::Warning
} else {
DiagnosticSeverity::Info
};
return Some(Diagnostic {
rule: RuleCode::FST016,
severity,
file: file.clone(),
range: Range::point(line_num + 1, 1),
message: format!(
"High z3rlimit ({}) detected. Consider using --split_queries always \
or extracting helper lemmas to reduce SMT solver load.",
rlimit
),
fix: None,
});
}
}
}
None
}
fn check_quake(&self, file: &PathBuf, line_num: usize, line: &str) -> Option<Diagnostic> {
if QUAKE_OPTION.is_match(line) {
return Some(Diagnostic {
rule: RuleCode::FST016,
severity: DiagnosticSeverity::Hint,
file: file.clone(),
range: Range::point(line_num + 1, 1),
message: "Using --quake for proof stability testing. \
This is fine for CI; consider removing if proof is now stable."
.to_string(),
fix: None,
});
}
None
}
fn check_retry(&self, file: &PathBuf, line_num: usize, line: &str) -> Option<Diagnostic> {
if RETRY_OPTION.is_match(line) {
return Some(Diagnostic {
rule: RuleCode::FST016,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(line_num + 1, 1),
message: "Using --retry for flaky verification. \
Acceptable during development; consider stabilizing for production."
.to_string(),
fix: None,
});
}
None
}
}
impl Default for PerfProfilerRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for PerfProfilerRule {
fn code(&self) -> RuleCode {
RuleCode::FST016
}
fn check(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("(*") {
continue;
}
if let Some(diag) = self.check_fuel(file, line_num, line) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_ifuel(file, line_num, line) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_rlimit(file, line_num, line) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_quake(file, line_num, line) {
diagnostics.push(diag);
}
if let Some(diag) = self.check_retry(file, line_num, line) {
diagnostics.push(diag);
}
}
let assert_count = MANY_ASSERTS.find_iter(content).count();
if assert_count > self.assert_count_threshold {
diagnostics.push(Diagnostic {
rule: RuleCode::FST016,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(1, 1),
message: format!(
"File has {} assertions. High assertion density ({} > {}) may slow \
verification. Consider splitting into smaller modules.",
assert_count, assert_count, self.assert_count_threshold
),
fix: None,
});
}
let smtpat_count = SMTPAT_MANY.find_iter(content).count();
if smtpat_count > 20 {
diagnostics.push(Diagnostic {
rule: RuleCode::FST016,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::point(1, 1),
message: format!(
"File has {} SMTPat annotations. Many patterns can cause quantifier \
instantiation explosion. Review pattern selectivity.",
smtpat_count
),
fix: None,
});
}
diagnostics
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_file() -> PathBuf {
PathBuf::from("test.fst")
}
#[test]
fn test_fuel_below_threshold_no_warning() {
let rule = PerfProfilerRule::new(); for fuel in [0, 1, 2, 3, 4, 5] {
let content = format!("#push-options \"--fuel {}\"", fuel);
let diagnostics = rule.check(&test_file(), &content);
assert!(
diagnostics.is_empty(),
"fuel {} should not trigger a warning",
fuel
);
}
}
#[test]
fn test_fuel_mildly_elevated_is_info() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--fuel 6\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Info);
assert!(diagnostics[0].message.contains("6"));
}
#[test]
fn test_fuel_high_is_warning() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--fuel 10\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
assert!(diagnostics[0].message.contains("10"));
}
#[test]
fn test_ifuel_below_threshold_no_warning() {
let rule = PerfProfilerRule::new(); for ifuel in [0, 1, 2, 3] {
let content = format!("#push-options \"--ifuel {}\"", ifuel);
let diagnostics = rule.check(&test_file(), &content);
assert!(
diagnostics.is_empty(),
"ifuel {} should not trigger a warning",
ifuel
);
}
}
#[test]
fn test_ifuel_mildly_elevated_is_info() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--ifuel 4\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Info);
assert!(diagnostics[0].message.contains("4"));
}
#[test]
fn test_ifuel_high_is_warning() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--ifuel 5\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
assert!(diagnostics[0].message.contains("5"));
}
#[test]
fn test_rlimit_common_values_no_warning() {
let rule = PerfProfilerRule::new(); for rlimit in [10, 20, 30, 40, 50, 60, 75, 80, 100, 150, 200, 250, 300] {
let content = format!("#push-options \"--z3rlimit {}\"", rlimit);
let diagnostics = rule.check(&test_file(), &content);
assert!(
diagnostics.is_empty(),
"z3rlimit {} should not trigger a warning (common in crypto proofs)",
rlimit
);
}
}
#[test]
fn test_rlimit_elevated_is_info() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 400\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Info);
assert!(diagnostics[0].message.contains("400"));
}
#[test]
fn test_rlimit_high_is_warning() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 700\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
assert!(diagnostics[0].message.contains("700"));
}
#[test]
fn test_rlimit_extreme_is_error() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 2000\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error);
assert!(diagnostics[0].message.contains("2000"));
}
#[test]
fn test_rlimit_boundary_600_is_info() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 600\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Info);
}
#[test]
fn test_rlimit_boundary_601_is_warning() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 601\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
}
#[test]
fn test_rlimit_boundary_900_is_warning() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 900\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
}
#[test]
fn test_rlimit_boundary_901_is_error() {
let rule = PerfProfilerRule::new(); let content = "#push-options \"--z3rlimit 901\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error);
}
#[test]
fn test_quake_is_hint() {
let rule = PerfProfilerRule::new();
let content = "#push-options \"--quake 3/5\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint);
assert!(diagnostics[0].message.contains("stability"));
}
#[test]
fn test_retry_is_info() {
let rule = PerfProfilerRule::new();
let content = "#push-options \"--retry 3\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Info);
assert!(diagnostics[0].message.contains("retry"));
}
#[test]
fn test_many_asserts_detection() {
let rule = PerfProfilerRule::with_thresholds(5, 3, 300, 5);
let content = r#"
let test () =
assert (1 = 1);
assert (2 = 2);
assert (3 = 3);
assert (4 = 4);
assert (5 = 5);
assert (6 = 6);
()
"#;
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("assertions"));
assert!(diagnostics[0].message.contains("6"));
}
#[test]
fn test_smtpat_count() {
let rule = PerfProfilerRule::new();
let mut content = String::new();
for i in 0..25 {
content.push_str(&format!(
"val lemma_{} : x:int -> Lemma (requires True) (ensures True) [SMTPat (x + {})]\n",
i, i
));
}
let diagnostics = rule.check(&test_file(), &content);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("SMTPat"));
assert!(diagnostics[0].message.contains("25"));
}
#[test]
fn test_multiple_issues_on_one_line() {
let rule = PerfProfilerRule::new();
let content = "#push-options \"--fuel 10 --ifuel 5 --z3rlimit 400 --quake 3/5\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 4); }
#[test]
fn test_comments_skipped() {
let rule = PerfProfilerRule::new();
let content = r#"
// #push-options "--fuel 10"
(* --fuel 10 *)
let normal () = ()
"#;
let diagnostics = rule.check(&test_file(), content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_custom_thresholds() {
let rule = PerfProfilerRule::with_thresholds(2, 1, 50, 10);
let content = "#push-options \"--fuel 3 --ifuel 2 --z3rlimit 60\"";
let diagnostics = rule.check(&test_file(), content);
assert_eq!(diagnostics.len(), 3); }
#[test]
fn test_hacl_star_common_pattern_no_warning() {
let rule = PerfProfilerRule::new();
let content = r#"
#push-options "--fuel 0 --ifuel 0 --z3rlimit 50"
let poly1305_update (ctx: poly_ctx) (block: lbytes 16) : poly_ctx =
let acc = ctx.acc in
let r = ctx.r in
(acc +. encode block) *. r
#pop-options
"#;
let diagnostics = rule.check(&test_file(), content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_hacl_star_elevated_rlimit_no_warning() {
let rule = PerfProfilerRule::new();
let content = r#"
#push-options "--fuel 1 --ifuel 0 --z3rlimit 200"
let lemma_chacha20_quarter_round a b c d s =
()
#pop-options
"#;
let diagnostics = rule.check(&test_file(), content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_severity_proportional_to_value() {
let rule = PerfProfilerRule::new();
let d = rule.check_fuel(&test_file(), 0, "--fuel 6").unwrap();
assert_eq!(d.severity, DiagnosticSeverity::Info);
let d = rule.check_fuel(&test_file(), 0, "--fuel 9").unwrap();
assert_eq!(d.severity, DiagnosticSeverity::Warning);
let d = rule.check_rlimit(&test_file(), 0, "--z3rlimit 400").unwrap();
assert_eq!(d.severity, DiagnosticSeverity::Info);
let d = rule.check_rlimit(&test_file(), 0, "--z3rlimit 700").unwrap();
assert_eq!(d.severity, DiagnosticSeverity::Warning);
let d = rule.check_rlimit(&test_file(), 0, "--z3rlimit 1000").unwrap();
assert_eq!(d.severity, DiagnosticSeverity::Error);
}
}