use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
use super::rules::{Diagnostic, DiagnosticSeverity, Edit, Fix, FixConfidence, FixSafetyLevel, Range, Rule, RuleCode};
lazy_static! {
static ref REFINEMENT: Regex = Regex::new(
r"(\w+)\s*:\s*(\w+)\s*\{([^}]+)\}"
).unwrap();
static ref TRUE_REFINEMENT: Regex = Regex::new(
r"(\w+)\s*:\s*(\w+)\s*\{\s*True\s*\}"
).unwrap();
static ref UNSAT_GT_LT: Regex = Regex::new(
r"(\w+)\s*>\s*(\d+)\s*/\\\s*(\w+)\s*<\s*(\d+)"
).unwrap();
static ref UNSAT_LT_GT: Regex = Regex::new(
r"(\w+)\s*<\s*(\d+)\s*/\\\s*(\w+)\s*>\s*(\d+)"
).unwrap();
}
fn byte_to_char_col(s: &str, byte_offset: usize) -> usize {
s[..byte_offset.min(s.len())].chars().count() + 1
}
fn is_inside_string_or_comment(line: &str, byte_offset: usize) -> bool {
let bytes = line.as_bytes();
let mut in_string = false;
let mut in_block_comment = false;
let mut i = 0;
while i < bytes.len() && i < byte_offset {
if in_string {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'"' {
in_string = false;
}
} else if in_block_comment {
if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b')' {
in_block_comment = false;
i += 2;
continue;
}
} else {
if bytes[i] == b'"' {
in_string = true;
} else if bytes[i] == b'(' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
in_block_comment = true;
i += 2;
continue;
}
}
i += 1;
}
in_string || in_block_comment
}
pub struct RefinementSimplifierRule;
impl RefinementSimplifierRule {
pub fn new() -> Self {
Self
}
fn is_gte_zero_refinement(var_name: &str, body: &str) -> bool {
let trimmed = body.trim();
let pattern = format!("{} >= 0", var_name);
trimmed == pattern || trimmed == format!("{}>=0", var_name)
}
fn is_gt_zero_refinement(var_name: &str, body: &str) -> bool {
let trimmed = body.trim();
let pattern = format!("{} > 0", var_name);
trimmed == pattern || trimmed == format!("{}>0", var_name)
}
fn check_redundant_nat(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for caps in REFINEMENT.captures_iter(line) {
let full_match = caps.get(0).unwrap();
if is_inside_string_or_comment(line, full_match.start()) {
continue;
}
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let base_type = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let body = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if base_type == "nat" && Self::is_gte_zero_refinement(var_name, body) {
let start_col = byte_to_char_col(line, full_match.start());
let end_col = byte_to_char_col(line, full_match.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Warning,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"Redundant refinement: `nat` is defined as `x:int{{x >= 0}}`, \
so `{} >= 0` adds no constraint. Use `{}:nat` instead.",
var_name, var_name
),
fix: Some(Fix::safe(
"Remove redundant refinement (safe: type unchanged)",
vec![Edit {
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
new_text: format!("{}:nat", var_name),
}],
)
.with_safety_level(FixSafetyLevel::Safe) .with_reversible(true) .with_requires_review(false)), });
}
}
}
diagnostics
}
fn check_redundant_pos(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for caps in REFINEMENT.captures_iter(line) {
let full_match = caps.get(0).unwrap();
if is_inside_string_or_comment(line, full_match.start()) {
continue;
}
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let base_type = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let body = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if base_type == "pos" && Self::is_gt_zero_refinement(var_name, body) {
let start_col = byte_to_char_col(line, full_match.start());
let end_col = byte_to_char_col(line, full_match.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Warning,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"Redundant refinement: `pos` is defined as `x:int{{x > 0}}`, \
so `{} > 0` adds no constraint. Use `{}:pos` instead.",
var_name, var_name
),
fix: Some(Fix::safe(
"Remove redundant refinement (safe: type unchanged)",
vec![Edit {
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
new_text: format!("{}:pos", var_name),
}],
)
.with_safety_level(FixSafetyLevel::Safe) .with_reversible(true) .with_requires_review(false)), });
}
}
}
diagnostics
}
fn check_true_refinement(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for caps in TRUE_REFINEMENT.captures_iter(line) {
let full_match = caps.get(0).unwrap();
if is_inside_string_or_comment(line, full_match.start()) {
continue;
}
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("x");
let type_name = caps.get(2).map(|m| m.as_str()).unwrap_or("T");
let start_col = byte_to_char_col(line, full_match.start());
let end_col = byte_to_char_col(line, full_match.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Warning,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"Useless refinement: `{{True}}` is always satisfied, adding no constraint. \
Use `{}:{}` instead.",
var_name, type_name
),
fix: Some(Fix::safe(
"Remove useless {True} refinement (safe: True is tautology)",
vec![Edit {
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
new_text: format!("{}:{}", var_name, type_name),
}],
)
.with_safety_level(FixSafetyLevel::Safe) .with_reversible(true) .with_requires_review(false)), });
}
}
diagnostics
}
fn check_int_to_nat(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for caps in REFINEMENT.captures_iter(line) {
let full_match = caps.get(0).unwrap();
if is_inside_string_or_comment(line, full_match.start()) {
continue;
}
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let base_type = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let body = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if base_type == "int" && Self::is_gte_zero_refinement(var_name, body) {
let start_col = byte_to_char_col(line, full_match.start());
let end_col = byte_to_char_col(line, full_match.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"Consider using `nat` instead of `int{{{} >= 0}}`. \
The `nat` type is defined as `x:int{{x >= 0}}` in Prims. \
NOTE: This changes the type, which may affect type inference. \
Keep the verbose form if it aids documentation or if dependent \
code expects the explicit refinement.",
var_name
),
fix: Some(Fix::unsafe_fix(
"Use nat instead of int{x >= 0} (CAUTION: changes type)",
vec![Edit {
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
new_text: format!("{}:nat", var_name),
}],
FixConfidence::Low,
"Type change from int{...} to nat may affect type inference",
)
.with_safety_level(FixSafetyLevel::Caution) .with_reversible(true) .with_requires_review(true)), });
}
}
}
diagnostics
}
fn check_int_to_pos(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for caps in REFINEMENT.captures_iter(line) {
let full_match = caps.get(0).unwrap();
if is_inside_string_or_comment(line, full_match.start()) {
continue;
}
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let base_type = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let body = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if base_type == "int" && Self::is_gt_zero_refinement(var_name, body) {
let start_col = byte_to_char_col(line, full_match.start());
let end_col = byte_to_char_col(line, full_match.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Info,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"Consider using `pos` instead of `int{{{} > 0}}`. \
The `pos` type is defined as `x:int{{x > 0}}` in Prims. \
NOTE: This changes the type, which may affect type inference. \
Keep the verbose form if it aids documentation or if dependent \
code expects the explicit refinement.",
var_name
),
fix: Some(Fix::unsafe_fix(
"Use pos instead of int{x > 0} (CAUTION: changes type)",
vec![Edit {
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
new_text: format!("{}:pos", var_name),
}],
FixConfidence::Low,
"Type change from int{...} to pos may affect type inference",
)
.with_safety_level(FixSafetyLevel::Caution) .with_reversible(true) .with_requires_review(true)), });
}
}
}
diagnostics
}
fn check_unsatisfiable(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if trimmed.starts_with("(*") || trimmed.starts_with("//") {
continue;
}
for ref_caps in REFINEMENT.captures_iter(line) {
let ref_match = ref_caps.get(0).unwrap();
if is_inside_string_or_comment(line, ref_match.start()) {
continue;
}
let body = ref_caps.get(3).map(|m| m.as_str()).unwrap_or("");
let body_start_byte = ref_caps.get(3).unwrap().start();
for caps in UNSAT_GT_LT.captures_iter(body) {
let var1 = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let var2 = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if var1 != var2 {
continue;
}
if let (Ok(lower), Ok(upper)) = (
caps.get(2).unwrap().as_str().parse::<i64>(),
caps.get(4).unwrap().as_str().parse::<i64>(),
) {
if lower >= upper {
let inner = caps.get(0).unwrap();
let start_col =
byte_to_char_col(line, body_start_byte + inner.start());
let end_col =
byte_to_char_col(line, body_start_byte + inner.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Error,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"UNSATISFIABLE REFINEMENT: No value can satisfy \
`{} > {} /\\ {} < {}`. \
This constraint is logically impossible.",
var1, lower, var2, upper
),
fix: None,
});
}
}
}
for caps in UNSAT_LT_GT.captures_iter(body) {
let var1 = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let var2 = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if var1 != var2 {
continue;
}
if let (Ok(upper), Ok(lower)) = (
caps.get(2).unwrap().as_str().parse::<i64>(),
caps.get(4).unwrap().as_str().parse::<i64>(),
) {
if lower >= upper {
let inner = caps.get(0).unwrap();
let start_col =
byte_to_char_col(line, body_start_byte + inner.start());
let end_col =
byte_to_char_col(line, body_start_byte + inner.end());
diagnostics.push(Diagnostic {
rule: RuleCode::FST012,
severity: DiagnosticSeverity::Error,
file: file.clone(),
range: Range::new(line_num, start_col, line_num, end_col),
message: format!(
"UNSATISFIABLE REFINEMENT: No value can satisfy \
`{} < {} /\\ {} > {}`. \
This constraint is logically impossible.",
var1, upper, var2, lower
),
fix: None,
});
}
}
}
}
}
diagnostics
}
}
impl Default for RefinementSimplifierRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for RefinementSimplifierRule {
fn code(&self) -> RuleCode {
RuleCode::FST012
}
fn check(&self, file: &PathBuf, content: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
diagnostics.extend(self.check_redundant_nat(file, content));
diagnostics.extend(self.check_redundant_pos(file, content));
diagnostics.extend(self.check_true_refinement(file, content));
diagnostics.extend(self.check_int_to_nat(file, content));
diagnostics.extend(self.check_int_to_pos(file, content));
diagnostics.extend(self.check_unsatisfiable(file, content));
diagnostics
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_path() -> PathBuf {
PathBuf::from("test.fst")
}
#[test]
fn test_redundant_nat_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Redundant refinement"));
assert!(diags[0].message.contains("nat"));
assert!(diags[0].fix.is_some());
}
#[test]
fn test_redundant_pos_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let bar (n:pos{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Redundant refinement"));
assert!(diags[0].message.contains("pos"));
}
#[test]
fn test_true_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let baz (x:int{True}) = x + 1";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Useless refinement"));
}
#[test]
fn test_int_to_nat() {
let rule = RefinementSimplifierRule::new();
let content = "let len (n:int{n >= 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Consider using `nat`"));
assert_eq!(diags[0].severity, DiagnosticSeverity::Info);
}
#[test]
fn test_int_to_pos() {
let rule = RefinementSimplifierRule::new();
let content = "let positive (n:int{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Consider using `pos`"));
}
#[test]
fn test_unsatisfiable_gt_lt() {
let rule = RefinementSimplifierRule::new();
let content = "let impossible (x:int{x > 5 /\\ x < 3}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("UNSATISFIABLE"));
assert_eq!(diags[0].severity, DiagnosticSeverity::Error);
}
#[test]
fn test_unsatisfiable_equal_bounds() {
let rule = RefinementSimplifierRule::new();
let content = "let also_impossible (x:int{x > 3 /\\ x < 3}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("UNSATISFIABLE"));
}
#[test]
fn test_satisfiable_range() {
let rule = RefinementSimplifierRule::new();
let content = "let valid_range (x:int{x > 3 /\\ x < 5}) = x";
let diags = rule.check(&make_path(), content);
assert!(!diags.iter().any(|d| d.message.contains("UNSATISFIABLE")));
}
#[test]
fn test_valid_nat_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded (x:nat{x < 100}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_comment_lines_skipped() {
let rule = RefinementSimplifierRule::new();
let content = "// let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_nat_refinement_fix_content() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().unwrap();
assert_eq!(fix.edits.len(), 1);
assert_eq!(fix.edits[0].new_text, "x:nat");
assert!(fix.message.contains("Remove redundant refinement"));
assert!(fix.message.contains("safe"));
}
#[test]
fn test_pos_refinement_fix_content() {
let rule = RefinementSimplifierRule::new();
let content = "let bar (n:pos{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().unwrap();
assert_eq!(fix.edits[0].new_text, "n:pos");
}
#[test]
fn test_true_refinement_fix_content() {
let rule = RefinementSimplifierRule::new();
let content = "let baz (x:int{True}) = x + 1";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().unwrap();
assert_eq!(fix.edits[0].new_text, "x:int");
assert!(fix.message.contains("Remove useless {True} refinement"));
assert!(fix.message.contains("safe"));
}
#[test]
fn test_int_to_nat_fix_content() {
let rule = RefinementSimplifierRule::new();
let content = "let len (n:int{n >= 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().unwrap();
assert_eq!(fix.edits[0].new_text, "n:nat");
}
#[test]
fn test_int_to_pos_fix_content() {
let rule = RefinementSimplifierRule::new();
let content = "let positive (n:int{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().unwrap();
assert_eq!(fix.edits[0].new_text, "n:pos");
}
#[test]
fn test_nat_pow2_32_no_longer_triggers() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded32 (x:nat{x < pow2 32}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_nat_pow2_64_no_longer_triggers() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded64 (n:nat{n < pow2 64}) = n";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_column_calculation_simple() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].range.start_col, 10);
}
#[test]
fn test_column_calculation_with_leading_spaces() {
let rule = RefinementSimplifierRule::new();
let content = " let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].range.start_col, 14);
}
#[test]
fn test_byte_to_char_col_ascii() {
let s = "hello world";
assert_eq!(byte_to_char_col(s, 0), 1); assert_eq!(byte_to_char_col(s, 5), 6); assert_eq!(byte_to_char_col(s, 11), 12); }
#[test]
fn test_byte_to_char_col_utf8() {
let s = "hi there";
assert_eq!(byte_to_char_col(s, 0), 1);
assert_eq!(byte_to_char_col(s, 2), 3);
}
#[test]
fn test_byte_to_char_col_edge_cases() {
let s = "abc";
assert_eq!(byte_to_char_col(s, 100), 4);
assert_eq!(byte_to_char_col(s, 0), 1);
}
#[test]
fn test_no_space_in_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x>=0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Redundant refinement"));
}
#[test]
fn test_extra_spaces_in_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{ x >= 0 }) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Redundant refinement"));
}
#[test]
fn test_multiline_not_detected() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{\nx >= 0\n}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_multiple_refinements_same_line() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) (y:pos{y > 0}) = x + y";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 2);
}
#[test]
fn test_multiple_refinements_different_lines() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) = x\nlet bar (y:int{y >= 0}) = y";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 2);
assert_eq!(diags[0].range.start_line, 1);
assert_eq!(diags[1].range.start_line, 2);
}
#[test]
fn test_block_comment_skipped() {
let rule = RefinementSimplifierRule::new();
let content = "(* let foo (x:nat{x >= 0}) = x *)";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_unsatisfiable_reversed_order() {
let rule = RefinementSimplifierRule::new();
let content = "let impossible (x:int{x < 3 /\\ x > 5}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("UNSATISFIABLE"));
}
#[test]
fn test_unsatisfiable_no_fix_provided() {
let rule = RefinementSimplifierRule::new();
let content = "let impossible (x:int{x > 5 /\\ x < 3}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(diags[0].fix.is_none());
}
#[test]
fn test_different_variables_not_unsatisfiable() {
let rule = RefinementSimplifierRule::new();
let content = "let valid (x:int{x > 5 /\\ y < 3}) = x";
let diags = rule.check(&make_path(), content);
assert!(!diags.iter().any(|d| d.message.contains("UNSATISFIABLE")));
}
#[test]
fn test_no_false_positive_nat_with_constraint() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded (x:nat{x < 10}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_no_false_positive_pos_with_constraint() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded (x:pos{x <= 100}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_no_false_positive_int_with_other_constraint() {
let rule = RefinementSimplifierRule::new();
let content = "let bounded (x:int{x < 100}) = x";
let diags = rule.check(&make_path(), content);
assert!(diags.is_empty());
}
#[test]
fn test_no_false_positive_string_literal() {
let rule = RefinementSimplifierRule::new();
let content = r#"let msg = "x:nat{x >= 0}" in msg"#;
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not flag refinement patterns inside string literals"
);
}
#[test]
fn test_no_false_positive_inline_block_comment() {
let rule = RefinementSimplifierRule::new();
let content = "let f = (* x:nat{x >= 0} *) 42";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not flag refinement patterns inside inline block comments"
);
}
#[test]
fn test_no_false_positive_unsatisfiable_outside_refinement() {
let rule = RefinementSimplifierRule::new();
let content = "let _ = assert (x > 5 /\\ x < 3)";
let diags = rule.check(&make_path(), content);
assert!(
!diags.iter().any(|d| d.message.contains("UNSATISFIABLE")),
"Should not flag unsatisfiable patterns outside refinement braces"
);
}
#[test]
fn test_no_false_positive_compound_refinement_nat() {
let rule = RefinementSimplifierRule::new();
let content = r"let byte (x:int{x >= 0 /\ x < 256}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not suggest nat for compound refinements"
);
}
#[test]
fn test_no_false_positive_compound_refinement_pos() {
let rule = RefinementSimplifierRule::new();
let content = r"let positive_byte (x:int{x > 0 /\ x <= 255}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not suggest pos for compound refinements"
);
}
#[test]
fn test_no_false_positive_string_literal_unsatisfiable() {
let rule = RefinementSimplifierRule::new();
let content = r#"let msg = "x:int{x > 5 /\ x < 3}" in msg"#;
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not flag unsatisfiable patterns inside string literals"
);
}
#[test]
fn test_no_false_positive_true_in_string() {
let rule = RefinementSimplifierRule::new();
let content = r#"let msg = "x:int{True}" in msg"#;
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not flag {{True}} inside string literals"
);
}
#[test]
fn test_is_inside_string_or_comment_basic() {
assert!(is_inside_string_or_comment(r#"foo "bar" baz"#, 5));
assert!(!is_inside_string_or_comment(r#"foo "bar" baz"#, 0));
assert!(!is_inside_string_or_comment(r#"foo "bar" baz"#, 10));
}
#[test]
fn test_is_inside_block_comment() {
assert!(is_inside_string_or_comment("foo (* bar *) baz", 7));
assert!(!is_inside_string_or_comment("foo (* bar *) baz", 0));
assert!(!is_inside_string_or_comment("foo (* bar *) baz", 14));
}
#[test]
fn test_is_inside_escaped_string() {
let line = r#"let s = "x:nat{x >= 0}\"more" in s"#;
assert!(is_inside_string_or_comment(line, 10));
}
#[test]
fn test_hacl_star_pattern_no_false_positive() {
let rule = RefinementSimplifierRule::new();
let content = "val lemma_mont_one: n:pos -> r:pos -> d:int{r * d % n = 1} -> a:nat";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Should not flag complex refinements from real hacl-star code"
);
}
#[test]
fn test_redundant_nat_fix_is_safe_and_auto_applicable() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:nat{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().expect("Should have a fix");
assert!(fix.is_safe, "Redundant nat removal should be marked safe");
assert!(
fix.can_auto_apply(),
"Safe high-confidence fix should be auto-applicable"
);
assert!(
fix.message.contains("safe"),
"Fix message should indicate safety"
);
}
#[test]
fn test_redundant_pos_fix_is_safe_and_auto_applicable() {
let rule = RefinementSimplifierRule::new();
let content = "let bar (n:pos{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().expect("Should have a fix");
assert!(fix.is_safe, "Redundant pos removal should be marked safe");
assert!(
fix.can_auto_apply(),
"Safe high-confidence fix should be auto-applicable"
);
}
#[test]
fn test_true_refinement_fix_is_safe_and_auto_applicable() {
let rule = RefinementSimplifierRule::new();
let content = "let baz (x:int{True}) = x + 1";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().expect("Should have a fix");
assert!(fix.is_safe, "True refinement removal should be marked safe");
assert!(
fix.can_auto_apply(),
"Safe high-confidence fix should be auto-applicable"
);
}
#[test]
fn test_int_to_nat_fix_is_unsafe_not_auto_applicable() {
let rule = RefinementSimplifierRule::new();
let content = "let len (n:int{n >= 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().expect("Should have a fix");
assert!(
!fix.is_safe,
"int->nat type change should be marked UNSAFE"
);
assert!(
!fix.can_auto_apply(),
"Unsafe fix should NOT be auto-applicable"
);
assert!(
fix.unsafe_reason.is_some(),
"Unsafe fix should have a reason"
);
assert!(
fix.message.contains("CAUTION"),
"Fix message should warn about type change"
);
}
#[test]
fn test_int_to_pos_fix_is_unsafe_not_auto_applicable() {
let rule = RefinementSimplifierRule::new();
let content = "let positive (n:int{n > 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
let fix = diags[0].fix.as_ref().expect("Should have a fix");
assert!(
!fix.is_safe,
"int->pos type change should be marked UNSAFE"
);
assert!(
!fix.can_auto_apply(),
"Unsafe fix should NOT be auto-applicable"
);
assert!(
fix.unsafe_reason.is_some(),
"Unsafe fix should have a reason"
);
}
#[test]
fn test_int_to_nat_message_warns_about_type_change() {
let rule = RefinementSimplifierRule::new();
let content = "let len (n:int{n >= 0}) = n";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(
diags[0].message.contains("NOTE"),
"Message should have a NOTE about implications"
);
assert!(
diags[0].message.contains("type inference"),
"Message should mention type inference impact"
);
}
#[test]
fn test_unsatisfiable_has_no_fix() {
let rule = RefinementSimplifierRule::new();
let content = "let impossible (x:int{x > 5 /\\ x < 3}) = x";
let diags = rule.check(&make_path(), content);
assert_eq!(diags.len(), 1);
assert!(
diags[0].fix.is_none(),
"Unsatisfiable refinement should have no fix"
);
assert_eq!(
diags[0].severity,
DiagnosticSeverity::Error,
"Unsatisfiable should be an error"
);
}
#[test]
fn test_reversed_gte_not_matched() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:int{0 <= x}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Reversed comparison form should not trigger (conservative)"
);
}
#[test]
fn test_not_int_base_type_no_simplification() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:integer{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Only exact 'int' base type should trigger simplification"
);
}
#[test]
fn test_qualified_int_not_matched() {
let rule = RefinementSimplifierRule::new();
let content = "let foo (x:FStar.Int.int{x >= 0}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Qualified types should not trigger simplification"
);
}
#[test]
fn test_conjunction_with_gte_zero_not_nat() {
let rule = RefinementSimplifierRule::new();
let content = r"let foo (x:int{x >= 0 /\ x <> 5}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"int{{x >= 0 /\\ extra}} should NOT suggest nat"
);
}
#[test]
fn test_disjunction_with_gte_zero_not_nat() {
let rule = RefinementSimplifierRule::new();
let content = r"let foo (x:int{x >= 0 \/ x < -10}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"int{{x >= 0 \\/ extra}} should NOT suggest nat"
);
}
#[test]
fn test_negation_with_gte_zero_not_nat() {
let rule = RefinementSimplifierRule::new();
let content = r"let foo (x:int{~(x >= 0)}) = x";
let diags = rule.check(&make_path(), content);
assert!(
diags.is_empty(),
"Negated constraint should NOT suggest nat"
);
}
}