use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use lazy_static::lazy_static;
use regex::Regex;
use super::parser::{
build_dependency_graph, get_definition_order, parse_fstar_file, BlockType, DeclarationBlock,
};
lazy_static! {
static ref VAL_PATTERN: Regex = Regex::new(r"\bval\s+").unwrap();
static ref TYPE_PATTERN: Regex = Regex::new(r"\btype\s+").unwrap();
static ref LET_PATTERN: Regex = Regex::new(r"\blet\s+").unwrap();
static ref MODULE_PATTERN: Regex = Regex::new(r"\bmodule\s+([A-Z][\w.]*)").unwrap();
}
use super::rules::{Diagnostic, DiagnosticSeverity, Edit, Fix, FixConfidence, FixSafetyLevel, Range, Rule, RuleCode};
fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
if m == 0 {
return n;
}
if n == 0 {
return m;
}
let mut prev_row: Vec<usize> = (0..=n).collect();
let mut curr_row: Vec<usize> = vec![0; n + 1];
for i in 1..=m {
curr_row[0] = i;
for j in 1..=n {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
curr_row[j] = (prev_row[j] + 1) .min(curr_row[j - 1] + 1) .min(prev_row[j - 1] + cost); }
std::mem::swap(&mut prev_row, &mut curr_row);
}
prev_row[n]
}
fn find_typo_match<'a>(
name: &str,
candidates: &'a HashSet<String>,
threshold: usize,
) -> Option<(&'a str, usize)> {
let mut best_match: Option<(&str, usize)> = None;
for candidate in candidates {
if candidate == name {
continue;
}
let len_diff = (name.len() as isize - candidate.len() as isize).unsigned_abs();
if len_diff > threshold {
continue;
}
let dist = levenshtein_distance(name, candidate);
if dist <= threshold {
match best_match {
None => best_match = Some((candidate.as_str(), dist)),
Some((_, best_dist)) if dist < best_dist => {
best_match = Some((candidate.as_str(), dist))
}
_ => {}
}
}
}
best_match
}
#[derive(Debug, Default)]
pub struct InterfaceConsistencyResult {
pub orphan_declarations: Vec<(String, Option<String>, usize)>,
pub typo_matches: Vec<(String, String, usize)>,
}
pub fn analyze_interface_consistency(
fsti_content: &str,
fst_content: &str,
) -> InterfaceConsistencyResult {
let (_, fsti_blocks) = parse_fstar_file(fsti_content);
let fst_order = get_definition_order(fst_content);
let fst_names_set: HashSet<String> = fst_order.into_iter().collect();
let mut fsti_val_names_with_lines: Vec<(String, usize)> = Vec::new();
for block in &fsti_blocks {
if block.block_type == BlockType::Val {
for name in &block.names {
fsti_val_names_with_lines.push((name.clone(), block.start_line));
}
}
}
let mut result = InterfaceConsistencyResult::default();
for (fsti_name, line) in &fsti_val_names_with_lines {
if !fst_names_set.contains(fsti_name) {
let typo_match = find_typo_match(fsti_name, &fst_names_set, 2);
if let Some((match_name, dist)) = typo_match {
result
.typo_matches
.push((fsti_name.clone(), match_name.to_string(), dist));
result.orphan_declarations.push((
fsti_name.clone(),
Some(match_name.to_string()),
*line,
));
} else {
result
.orphan_declarations
.push((fsti_name.clone(), None, *line));
}
}
}
result
}
pub struct ReorderInterfaceRule;
impl ReorderInterfaceRule {
pub fn new() -> Self {
Self
}
}
impl Default for ReorderInterfaceRule {
fn default() -> Self {
Self::new()
}
}
fn check_order_valid(
order: &[String],
deps: &HashMap<String, HashSet<String>>,
all_declared_names: Option<&HashSet<String>>,
) -> (bool, Vec<String>) {
let mut violations = Vec::new();
let name_to_position: HashMap<&str, usize> = order
.iter()
.enumerate()
.map(|(i, name)| (name.as_str(), i))
.collect();
let order_set: HashSet<&str> = order.iter().map(|s| s.as_str()).collect();
for name in order {
if let Some(name_deps) = deps.get(name) {
let name_pos = name_to_position[name.as_str()];
for dep in name_deps {
let is_valid_dep = all_declared_names
.map(|names| names.contains(dep))
.unwrap_or(true);
if !is_valid_dep {
continue;
}
if !order_set.contains(dep.as_str()) {
violations.push(format!(
"'{}' references '{}', but '{}' is not in order \
(FSTI-only declaration that would be placed at END)",
name, dep, dep
));
} else if let Some(&dep_pos) = name_to_position.get(dep.as_str()) {
if dep_pos > name_pos {
violations.push(format!(
"'{}' references '{}', but '{}' comes after '{}'",
name, dep, dep, name
));
}
}
}
}
}
(violations.is_empty(), violations)
}
fn topological_sort_with_preference(
names: &[String],
deps: &HashMap<String, HashSet<String>>,
preferred_order: &[String],
) -> Result<Vec<String>, Vec<String>> {
let name_set: HashSet<&str> = names.iter().map(|s| s.as_str()).collect();
let mut filtered_deps: HashMap<&str, HashSet<&str>> = HashMap::new();
for name in names {
let mut local_deps = HashSet::new();
if let Some(name_deps) = deps.get(name) {
for dep in name_deps {
if name_set.contains(dep.as_str()) {
local_deps.insert(dep.as_str());
}
}
}
filtered_deps.insert(name.as_str(), local_deps);
}
let mut in_degree: HashMap<&str, usize> = names.iter().map(|n| (n.as_str(), 0)).collect();
for name in names {
for dep in filtered_deps.get(name.as_str()).unwrap_or(&HashSet::new()) {
if let Some(degree) = in_degree.get_mut(name.as_str()) {
*degree += 1;
}
}
}
let preferred_position: HashMap<&str, usize> = preferred_order
.iter()
.enumerate()
.map(|(i, name)| (name.as_str(), i))
.collect();
let get_priority = |name: &str| -> usize {
preferred_position
.get(name)
.copied()
.unwrap_or(preferred_order.len())
};
let mut available: Vec<&str> = names
.iter()
.filter(|n| in_degree.get(n.as_str()) == Some(&0))
.map(|s| s.as_str())
.collect();
available.sort_by_key(|n| get_priority(n));
let mut result = Vec::new();
while let Some(current) = available.first().cloned() {
available.remove(0);
result.push(current.to_string());
for name in names {
if filtered_deps
.get(name.as_str())
.map(|d| d.contains(current))
.unwrap_or(false)
{
if let Some(degree) = in_degree.get_mut(name.as_str()) {
*degree = degree.saturating_sub(1);
if *degree == 0 && !result.contains(&name.to_string()) {
available.push(name.as_str());
available.sort_by_key(|n| get_priority(n));
}
}
}
}
}
if result.len() != names.len() {
let remaining: Vec<&str> = names
.iter()
.filter(|n| !result.contains(&n.to_string()))
.map(|s| s.as_str())
.collect();
return Err(vec![format!(
"Cycle detected involving: {}",
remaining.join(", ")
)]);
}
Ok(result)
}
pub fn reorder_fsti_content(
fsti_content: &str,
_fst_content: &str,
) -> Result<(String, Vec<(String, usize, usize)>, bool), Vec<String>> {
let (fsti_header, fsti_blocks) = parse_fstar_file(fsti_content);
let deps = build_dependency_graph(&fsti_blocks);
let mut block_by_name: HashMap<&str, &DeclarationBlock> = HashMap::new();
for block in &fsti_blocks {
for name in &block.names {
if !block_by_name.contains_key(name.as_str()) {
block_by_name.insert(name.as_str(), block);
}
}
}
let mut fsti_names_set: HashSet<String> = HashSet::new();
for block in &fsti_blocks {
fsti_names_set.extend(block.names.iter().cloned());
}
let current_fsti_order: Vec<String> = fsti_blocks
.iter()
.flat_map(|b| b.names.iter().cloned())
.collect();
let (current_is_valid, current_violations) =
check_order_valid(¤t_fsti_order, &deps, Some(&fsti_names_set));
if current_is_valid {
return Ok((fsti_content.to_string(), Vec::new(), false));
}
let fsti_all_names: Vec<String> = fsti_names_set.iter().cloned().collect();
let relevant_order = match topological_sort_with_preference(
&fsti_all_names,
&deps,
¤t_fsti_order, ) {
Ok(sorted) => sorted,
Err(_) => return Err(current_violations),
};
let mut original_positions: HashMap<&str, usize> = HashMap::new();
for (idx, block) in fsti_blocks.iter().enumerate() {
for name in &block.names {
if !original_positions.contains_key(name.as_str()) {
original_positions.insert(name.as_str(), idx);
}
}
}
let mut reordered_blocks: Vec<&DeclarationBlock> = Vec::new();
let mut used_block_indices: HashSet<usize> = HashSet::new();
let mut movements: Vec<(String, usize, usize)> = Vec::new();
let mut new_position = 0;
for name in &relevant_order {
if let Some(&block) = block_by_name.get(name.as_str()) {
let block_idx = fsti_blocks
.iter()
.position(|b| std::ptr::eq(b, block))
.unwrap_or(0);
if !used_block_indices.contains(&block_idx) {
reordered_blocks.push(block);
used_block_indices.insert(block_idx);
let primary = block
.names
.first()
.map(|s| s.as_str())
.unwrap_or(name.as_str());
let old_pos = original_positions.get(primary).copied().unwrap_or(0);
if old_pos != new_position {
movements.push((primary.to_string(), old_pos, new_position));
}
new_position += 1;
}
}
}
for (idx, block) in fsti_blocks.iter().enumerate() {
if !used_block_indices.contains(&idx) {
reordered_blocks.push(block);
used_block_indices.insert(idx);
if let Some(primary) = block.names.first() {
let old_pos = original_positions
.get(primary.as_str())
.copied()
.unwrap_or(0);
movements.push((primary.clone(), old_pos, new_position));
}
new_position += 1;
}
}
let mut reordered_lines: Vec<String> = fsti_header.clone();
if !reordered_lines.is_empty() && !reordered_blocks.is_empty() {
if let Some(last) = reordered_lines.last() {
if !last.trim().is_empty() {
reordered_lines.push("\n".to_string());
}
}
}
for block in &reordered_blocks {
reordered_lines.extend(block.lines.iter().cloned());
}
let original_order: Vec<&str> = fsti_blocks
.iter()
.flat_map(|b| b.names.iter().map(|s| s.as_str()))
.collect();
let new_order: Vec<&str> = reordered_blocks
.iter()
.flat_map(|b| b.names.iter().map(|s| s.as_str()))
.collect();
let changed = original_order != new_order;
Ok((reordered_lines.concat(), movements, changed))
}
impl Rule for ReorderInterfaceRule {
fn code(&self) -> RuleCode {
RuleCode::FST002
}
fn check(&self, _file: &PathBuf, _content: &str) -> Vec<Diagnostic> {
vec![]
}
fn requires_pair(&self) -> bool {
true
}
fn check_pair(
&self,
_fst_file: &PathBuf,
fst_content: &str,
fsti_file: &PathBuf,
fsti_content: &str,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let consistency = analyze_interface_consistency(fsti_content, fst_content);
for (fsti_name, fst_name, distance) in &consistency.typo_matches {
let message = format!(
"Possible typo: `{}` declared in interface but `{}` defined in implementation \
(edit distance: {}). Did you mean `{}`?",
fsti_name, fst_name, distance, fst_name
);
let line = consistency
.orphan_declarations
.iter()
.find(|(n, _, _)| n == fsti_name)
.map(|(_, _, l)| *l)
.unwrap_or(1);
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Warning,
file: fsti_file.clone(),
range: Range::point(line, 1),
message,
fix: None,
});
}
for (name, typo_match, line) in &consistency.orphan_declarations {
if typo_match.is_none() {
let message = format!(
"Orphan declaration: `{}` is declared in interface but has no \
implementation in .fst. Is this intentional (assume val/type) \
or is the implementation missing?",
name
);
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Warning,
file: fsti_file.clone(),
range: Range::point(*line, 1),
message,
fix: None,
});
}
}
match reorder_fsti_content(fsti_content, fst_content) {
Ok((reordered_content, movements, changed)) => {
if !changed {
return diagnostics;
}
let fsti_path = fsti_file.to_string_lossy();
let validation_warnings =
validate_reorder(fsti_content, &reordered_content, &fsti_path);
let critical_warnings: Vec<_> = validation_warnings
.iter()
.filter(|w| w.severity == ReorderWarningSeverity::Critical)
.collect();
if !critical_warnings.is_empty() {
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Error,
file: fsti_file.clone(),
range: Range::point(1, 1),
message: format!(
"Interface has forward reference issues, but AUTOFIX IS BLOCKED due to {} critical validation error(s). \
Manual intervention required.",
critical_warnings.len()
),
fix: None, });
for warning in critical_warnings {
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Error,
file: fsti_file.clone(),
range: Range::point(1, 1),
message: format!("Validation failed: {}", warning.message),
fix: None,
});
}
return diagnostics;
}
let message = format!(
"Interface has forward reference issues. {} declaration{} need{} reordering to fix F* Error 233.",
movements.len(),
if movements.len() == 1 { "" } else { "s" },
if movements.len() == 1 { "s" } else { "" }
);
let fsti_lines: Vec<&str> = fsti_content.lines().collect();
let has_validation_warnings = !validation_warnings.is_empty();
let (confidence, is_safe, unsafe_reason) = if movements.len() > 10 {
(
FixConfidence::Low,
false,
Some(format!(
"Many declarations ({}) will be reordered. Manual review strongly recommended.",
movements.len()
)),
)
} else if movements.len() > 5 || has_validation_warnings {
(
FixConfidence::Medium,
false,
Some(format!(
"Moderate reordering ({} declarations). Please review before applying.",
movements.len()
)),
)
} else {
(FixConfidence::High, true, None)
};
let fix = Fix {
message: "Reorder declarations to fix forward references".to_string(),
edits: vec![Edit {
file: fsti_file.clone(),
range: Range::new(1, 1, fsti_lines.len() + 1, 1),
new_text: reordered_content,
}],
confidence,
is_safe,
unsafe_reason,
safety_level: FixSafetyLevel::Caution,
reversible: true, requires_review: true, };
let first_line = movements.first().map(|(_, old, _)| *old + 1).unwrap_or(1);
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Error,
file: fsti_file.clone(),
range: Range::point(first_line, 1),
message,
fix: Some(fix),
});
if movements.len() > 10 {
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Warning,
file: fsti_file.clone(),
range: Range::point(1, 1),
message: format!(
"WARNING: {} declarations will move. Review the changes carefully before applying.",
movements.len()
),
fix: None,
});
}
for (name, old_pos, new_pos) in &movements {
let direction = if new_pos > old_pos { "down" } else { "up" };
let detail_message = format!(
"`{}` needs to move {} (position {} -> {})",
name, direction, old_pos, new_pos
);
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Info,
file: fsti_file.clone(),
range: Range::point(old_pos + 1, 1),
message: detail_message,
fix: None,
});
}
for warning in validation_warnings
.iter()
.filter(|w| w.severity != ReorderWarningSeverity::Critical)
{
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Info,
file: fsti_file.clone(),
range: Range::point(1, 1),
message: format!("Validation note: {}", warning.message),
fix: None,
});
}
diagnostics
}
Err(errors) => {
for err in errors {
diagnostics.push(Diagnostic {
rule: RuleCode::FST002,
severity: DiagnosticSeverity::Error,
file: fsti_file.clone(),
range: Range::point(1, 1),
message: format!("Cannot reorder: {}", err),
fix: None,
});
}
diagnostics
}
}
}
}
#[derive(Debug, Clone)]
pub struct ReorderValidationWarning {
pub message: String,
pub severity: ReorderWarningSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReorderWarningSeverity {
Critical,
Warning,
Info,
}
fn compute_content_hash(content: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
let non_ws: String = content.chars().filter(|c| !c.is_whitespace()).collect();
non_ws.hash(&mut hasher);
hasher.finish()
}
fn extract_all_declaration_names(content: &str) -> Vec<String> {
let (_, blocks) = parse_fstar_file(content);
let mut names = Vec::new();
for block in &blocks {
names.extend(block.names.iter().cloned());
}
names
}
fn count_declaration_names(names: &[String]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for name in names {
*counts.entry(name.clone()).or_insert(0) += 1;
}
counts
}
pub fn validate_reorder(
original_content: &str,
reordered_content: &str,
_fsti_path: &str,
) -> Vec<ReorderValidationWarning> {
let mut warnings = Vec::new();
let orig_non_ws: String = original_content
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let new_non_ws: String = reordered_content
.chars()
.filter(|c| !c.is_whitespace())
.collect();
if orig_non_ws.len() != new_non_ws.len() {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Non-whitespace character count mismatch: {} -> {} (delta: {}). \
Content was lost or duplicated during reordering. FIX REFUSED.",
orig_non_ws.len(),
new_non_ws.len(),
new_non_ws.len() as i64 - orig_non_ws.len() as i64
),
severity: ReorderWarningSeverity::Critical,
});
}
let orig_names = extract_all_declaration_names(original_content);
let new_names = extract_all_declaration_names(reordered_content);
let orig_counts = count_declaration_names(&orig_names);
let new_counts = count_declaration_names(&new_names);
for (name, &orig_count) in &orig_counts {
let new_count = new_counts.get(name).copied().unwrap_or(0);
if new_count == 0 {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Declaration '{}' was LOST during reordering! \
Original had {} occurrence(s), reordered has none. FIX REFUSED.",
name, orig_count
),
severity: ReorderWarningSeverity::Critical,
});
} else if new_count != orig_count {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Declaration '{}' count changed: {} -> {}. \
Possible duplication or loss. FIX REFUSED.",
name, orig_count, new_count
),
severity: ReorderWarningSeverity::Critical,
});
}
}
for (name, &new_count) in &new_counts {
if !orig_counts.contains_key(name) {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Declaration '{}' appeared {} time(s) in reordered content \
but was NOT in original! Possible content corruption. FIX REFUSED.",
name, new_count
),
severity: ReorderWarningSeverity::Critical,
});
}
}
let orig_lines: Vec<&str> = original_content.lines().collect();
let new_lines: Vec<&str> = reordered_content.lines().collect();
let orig_count = orig_lines.len();
let new_count = new_lines.len();
let line_delta = (new_count as i64 - orig_count as i64).abs();
if line_delta > 10 {
warnings.push(ReorderValidationWarning {
message: format!(
"Line count changed significantly: {} -> {} (delta: {}). \
This may indicate content was lost or duplicated.",
orig_count, new_count, line_delta
),
severity: ReorderWarningSeverity::Warning,
});
}
let orig_module = MODULE_PATTERN
.captures(original_content)
.and_then(|c| c.get(1).map(|m| m.as_str()));
let new_module = MODULE_PATTERN
.captures(reordered_content)
.and_then(|c| c.get(1).map(|m| m.as_str()));
match (orig_module, new_module) {
(Some(orig), None) => {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Module declaration '{}' was LOST! FIX REFUSED.",
orig
),
severity: ReorderWarningSeverity::Critical,
});
}
(Some(orig), Some(new)) if orig != new => {
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Module name changed from '{}' to '{}'! FIX REFUSED.",
orig, new
),
severity: ReorderWarningSeverity::Critical,
});
}
_ => {}
}
let token_checks = [
(&*VAL_PATTERN, "val"),
(&*TYPE_PATTERN, "type"),
(&*LET_PATTERN, "let"),
];
for (pattern, name) in token_checks {
let orig_matches = pattern.find_iter(original_content).count();
let new_matches = pattern.find_iter(reordered_content).count();
if orig_matches != new_matches {
warnings.push(ReorderValidationWarning {
message: format!(
"{} keyword count changed: {} -> {} (delta: {})",
name,
orig_matches,
new_matches,
new_matches as i64 - orig_matches as i64
),
severity: ReorderWarningSeverity::Warning,
});
}
}
let mut orig_chars: Vec<char> = original_content
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let mut new_chars: Vec<char> = reordered_content
.chars()
.filter(|c| !c.is_whitespace())
.collect();
orig_chars.sort();
new_chars.sort();
if orig_chars != new_chars {
let diff_pos = orig_chars
.iter()
.zip(new_chars.iter())
.position(|(a, b)| a != b);
warnings.push(ReorderValidationWarning {
message: format!(
"CRITICAL: Character-level content mismatch! \
First difference at sorted position {:?}. \
Original has {} non-ws chars, reordered has {}. FIX REFUSED.",
diff_pos,
orig_chars.len(),
new_chars.len()
),
severity: ReorderWarningSeverity::Critical,
});
}
warnings
}
pub fn preflight_reorder_check(
original_content: &str,
) -> Result<PreflightReport, Vec<String>> {
let (header, blocks) = parse_fstar_file(original_content);
let mut errors = Vec::new();
let mut report = PreflightReport {
total_declarations: 0,
declaration_names: Vec::new(),
has_module_declaration: false,
header_lines: header.len(),
block_count: blocks.len(),
};
let has_module = header.iter().any(|line| line.trim().starts_with("module "));
report.has_module_declaration = has_module;
if !has_module {
errors.push("No module declaration found in header".to_string());
}
let mut all_names = Vec::new();
for block in &blocks {
all_names.extend(block.names.iter().cloned());
}
report.declaration_names = all_names.clone();
report.total_declarations = all_names.len();
let mut seen_in_blocks: HashMap<String, usize> = HashMap::new();
for (block_idx, block) in blocks.iter().enumerate() {
for name in &block.names {
if let Some(prev_block) = seen_in_blocks.get(name) {
if *prev_block != block_idx {
errors.push(format!(
"Declaration '{}' appears in multiple blocks (blocks {} and {})",
name, prev_block, block_idx
));
}
} else {
seen_in_blocks.insert(name.clone(), block_idx);
}
}
}
let parse_errors = super::parser::validate_parsing(&blocks);
for err in parse_errors {
if err.severity == super::parser::ParseErrorSeverity::Error {
errors.push(format!("Parse error at line {}: {}", err.line, err.message));
}
}
if errors.is_empty() {
Ok(report)
} else {
Err(errors)
}
}
#[derive(Debug, Clone)]
pub struct PreflightReport {
pub total_declarations: usize,
pub declaration_names: Vec<String>,
pub has_module_declaration: bool,
pub header_lines: usize,
pub block_count: usize,
}
#[derive(Debug, Clone)]
pub struct DryRunResult {
pub movements: Vec<MovementInfo>,
pub total_declarations: usize,
pub has_changes: bool,
pub warning_level: DryRunWarningLevel,
pub validation_warnings: Vec<ReorderValidationWarning>,
}
#[derive(Debug, Clone)]
pub struct MovementInfo {
pub name: String,
pub old_position: usize,
pub new_position: usize,
pub direction: MovementDirection,
pub distance: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MovementDirection {
Up,
Down,
NoChange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DryRunWarningLevel {
None,
Low,
Medium,
High,
}
pub fn dry_run_reorder(
fsti_content: &str,
fst_content: &str,
) -> Result<DryRunResult, Vec<String>> {
let preflight = preflight_reorder_check(fsti_content)?;
match reorder_fsti_content(fsti_content, fst_content) {
Ok((reordered_content, movements_raw, changed)) => {
let validation_warnings =
validate_reorder(fsti_content, &reordered_content, "dry-run");
let critical_count = validation_warnings
.iter()
.filter(|w| w.severity == ReorderWarningSeverity::Critical)
.count();
if critical_count > 0 {
let error_msgs: Vec<String> = validation_warnings
.iter()
.filter(|w| w.severity == ReorderWarningSeverity::Critical)
.map(|w| w.message.clone())
.collect();
return Err(error_msgs);
}
let movements: Vec<MovementInfo> = movements_raw
.iter()
.map(|(name, old_pos, new_pos)| {
let direction = if new_pos > old_pos {
MovementDirection::Down
} else if new_pos < old_pos {
MovementDirection::Up
} else {
MovementDirection::NoChange
};
let distance = (*new_pos as i64 - *old_pos as i64).unsigned_abs() as usize;
MovementInfo {
name: name.clone(),
old_position: *old_pos,
new_position: *new_pos,
direction,
distance,
}
})
.collect();
let moving_count = movements
.iter()
.filter(|m| m.direction != MovementDirection::NoChange)
.count();
let warning_level = if !changed || moving_count == 0 {
DryRunWarningLevel::None
} else if moving_count <= 3 {
DryRunWarningLevel::Low
} else if moving_count <= 10 {
DryRunWarningLevel::Medium
} else {
DryRunWarningLevel::High
};
Ok(DryRunResult {
movements,
total_declarations: preflight.total_declarations,
has_changes: changed,
warning_level,
validation_warnings,
})
}
Err(errors) => Err(errors),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_reorder_no_changes() {
let content = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let warnings = validate_reorder(content, content, "Test.fsti");
assert!(warnings.is_empty());
}
#[test]
fn test_validate_reorder_lost_module_legacy() {
let original = r#"module Test
val foo: int -> int
"#;
let reordered = r#"
val foo: int -> int
"#;
let warnings = validate_reorder(original, reordered, "Test.fsti");
let has_critical = warnings
.iter()
.any(|w| w.severity == ReorderWarningSeverity::Critical);
assert!(
has_critical,
"Lost module should trigger critical warning. Got: {:?}",
warnings
);
}
#[test]
fn test_validate_reorder_lost_declaration_legacy() {
let original = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let reordered = r#"module Test
val foo: int -> int
"#;
let warnings = validate_reorder(original, reordered, "Test.fsti");
let has_critical = warnings
.iter()
.any(|w| w.severity == ReorderWarningSeverity::Critical);
assert!(
has_critical,
"Lost declaration should trigger critical warning. Got: {:?}",
warnings
);
}
#[test]
fn test_correct_order_no_change() {
let fst_content = r#"
module Test
let foo x = x + 1
let bar x = foo x
"#;
let fsti_content = r#"
module Test
val foo: int -> int
val bar: int -> int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (_, _, changed) = result.unwrap();
assert!(!changed);
}
#[test]
fn test_different_order_no_forward_ref_is_valid() {
let fst_content = r#"
module Test
let foo x = x + 1
let bar x = foo x
"#;
let fsti_content = r#"
module Test
val bar: int -> int
val foo: int -> int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (_, movements, changed) = result.unwrap();
assert!(!changed, "Expected no change when there are no forward references");
assert!(movements.is_empty());
}
#[test]
fn test_fsti_only_types_at_top_is_valid() {
let fst_content = r#"
module Test
let foo x = x + 1
"#;
let fsti_content = r#"
module Test
let t_limbs = int
val foo: t_limbs -> t_limbs
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (_, movements, changed) = result.unwrap();
assert!(!changed, "FSTI-only types at top should be valid");
assert!(movements.is_empty());
}
#[test]
fn test_forward_reference_needs_fix() {
let fst_content = r#"
module Test
type mytype = int
let foo (x: mytype) = x
"#;
let fsti_content = r#"
module Test
val foo: mytype -> mytype
type mytype = int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (reordered, movements, changed) = result.unwrap();
assert!(changed, "Expected change when there is a forward reference");
assert!(!movements.is_empty());
let mytype_pos = reordered.find("type mytype");
let foo_pos = reordered.find("val foo");
assert!(mytype_pos.is_some() && foo_pos.is_some());
assert!(mytype_pos.unwrap() < foo_pos.unwrap(), "mytype should come before foo");
}
#[test]
fn test_dependency_respected() {
let fst_content = r#"
module Test
type mytype = int
val uses_mytype: mytype -> int
"#;
let fsti_content = r#"
module Test
val uses_mytype: mytype -> int
type mytype = int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (reordered, _, changed) = result.unwrap();
assert!(changed);
let mytype_pos = reordered.find("type mytype");
let uses_pos = reordered.find("val uses_mytype");
assert!(mytype_pos.is_some() && uses_pos.is_some());
assert!(mytype_pos.unwrap() < uses_pos.unwrap());
}
#[test]
fn test_levenshtein_distance_identical() {
assert_eq!(levenshtein_distance("hello", "hello"), 0);
}
#[test]
fn test_levenshtein_distance_single_char_diff() {
assert_eq!(levenshtein_distance("baz", "bar"), 1);
assert_eq!(levenshtein_distance("cat", "car"), 1);
}
#[test]
fn test_levenshtein_distance_insertion() {
assert_eq!(levenshtein_distance("parser_kindnz", "parser_kind_nz"), 1);
}
#[test]
fn test_levenshtein_distance_deletion() {
assert_eq!(levenshtein_distance("hello", "helo"), 1);
}
#[test]
fn test_levenshtein_distance_multiple_edits() {
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
}
#[test]
fn test_levenshtein_distance_empty() {
assert_eq!(levenshtein_distance("", "abc"), 3);
assert_eq!(levenshtein_distance("abc", ""), 3);
assert_eq!(levenshtein_distance("", ""), 0);
}
#[test]
fn test_find_typo_match_exact_match_excluded() {
let candidates: HashSet<String> = ["foo", "bar", "baz"]
.iter()
.map(|s| s.to_string())
.collect();
let result = find_typo_match("foo", &candidates, 2);
assert!(result.is_none() || result.unwrap().0 != "foo");
}
#[test]
fn test_find_typo_match_finds_close_match() {
let candidates: HashSet<String> = ["parser_kind_nz", "foo", "bar"]
.iter()
.map(|s| s.to_string())
.collect();
let result = find_typo_match("parser_kindnz", &candidates, 2);
assert!(result.is_some());
let (match_name, dist) = result.unwrap();
assert_eq!(match_name, "parser_kind_nz");
assert_eq!(dist, 1);
}
#[test]
fn test_find_typo_match_no_match_above_threshold() {
let candidates: HashSet<String> = ["completely", "different", "names"]
.iter()
.map(|s| s.to_string())
.collect();
let result = find_typo_match("parser_kindnz", &candidates, 2);
assert!(result.is_none());
}
#[test]
fn test_analyze_interface_consistency_orphan_detection() {
let fsti_content = r#"
module Test
val foo: int -> int
val orphan_decl: int -> int
"#;
let fst_content = r#"
module Test
let foo x = x + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(!result.orphan_declarations.is_empty());
assert!(result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "orphan_decl"));
}
#[test]
fn test_analyze_interface_consistency_typo_detection() {
let fsti_content = r#"
module Test
val foo: int -> int
val parser_kindnz: int -> int
"#;
let fst_content = r#"
module Test
let foo x = x + 1
let parser_kind_nz x = x * 2
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(!result.typo_matches.is_empty());
let typo = result.typo_matches.iter().find(|(fsti, _, _)| fsti == "parser_kindnz");
assert!(typo.is_some());
let (_, fst_name, dist) = typo.unwrap();
assert_eq!(fst_name, "parser_kind_nz");
assert_eq!(*dist, 1);
}
#[test]
fn test_analyze_interface_consistency_no_issues() {
let fsti_content = r#"
module Test
val foo: int -> int
val bar: int -> int
"#;
let fst_content = r#"
module Test
let foo x = x + 1
let bar x = x * 2
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(result.orphan_declarations.is_empty());
assert!(result.typo_matches.is_empty());
}
#[test]
fn test_analyze_interface_consistency_similar_names_not_typo() {
let fsti_content = r#"
module Test
val process_data: int -> int
"#;
let fst_content = r#"
module Test
let handle_request x = x + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(result.typo_matches.is_empty());
assert!(result
.orphan_declarations
.iter()
.any(|(name, typo_match, _)| name == "process_data" && typo_match.is_none()));
}
#[test]
fn test_interface_only_let_type_aliases_not_orphans() {
let fsti_content = r#"
module Test
let bn_add_eq_len_st (t:int) (len:int) =
int -> int -> int
val bn_add_eq_len: int -> bn_add_eq_len_st int int
"#;
let fst_content = r#"
module Test
let bn_add_eq_len x a b = a + b
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
!result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "bn_add_eq_len_st"),
"Interface-only let type alias should NOT be flagged as orphan"
);
assert!(result.orphan_declarations.is_empty());
}
#[test]
fn test_interface_only_type_definitions_not_orphans() {
let fsti_content = r#"
module Test
type abstract_key
val encrypt: abstract_key -> int -> int
"#;
let fst_content = r#"
module Test
let encrypt k x = x + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
!result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "abstract_key"),
"Interface-only type definition should NOT be flagged as orphan"
);
}
#[test]
fn test_interface_only_class_not_orphan() {
let fsti_content = r#"
module Test
class bn (t:int) = {
len: int;
add: int -> int;
}
val mk_runtime_bn: int -> bn int
"#;
let fst_content = r#"
module Test
let mk_runtime_bn t = { len = 0; add = fun x -> x }
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
!result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "bn"),
"Interface-only class definition should NOT be flagged as orphan"
);
}
#[test]
fn test_interface_only_inline_let_not_orphan() {
let fsti_content = r#"
module Test
inline_for_extraction let meta_len (t:int) = int
val mk_runtime: meta_len int -> int
"#;
let fst_content = r#"
module Test
let mk_runtime len = len + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
!result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "meta_len"),
"Interface-only inline_for_extraction let should NOT be flagged as orphan"
);
}
#[test]
fn test_qualified_names_no_false_dependencies() {
let fst_content = r#"
module Test
type mytype = int
let bn_add x = x + 1
"#;
let fsti_content = r#"
module Test
let bn_add_st (t:int) =
int -> S.bn_add int
val bn_add: int -> int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (_, movements, changed) = result.unwrap();
assert!(
!changed,
"Qualified name S.bn_add should not create false dependency on local bn_add. \
Movements: {:?}",
movements
);
}
#[test]
fn test_many_interface_only_defs_no_warnings() {
let fsti_content = r#"
module Test
let add_st (t:int) (len:int) =
int -> int -> int
let sub_st (t:int) (len:int) =
int -> int -> int
let mul_st (t:int) =
int -> int -> int
type config_t = int
val add: int -> add_st int int
val sub: int -> sub_st int int
val mul: int -> mul_st int
"#;
let fst_content = r#"
module Test
let add t a b = a + b
let sub t a b = a - b
let mul t a b = a * b
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
result.orphan_declarations.is_empty(),
"Interface-only type aliases should not generate orphan warnings. \
Got: {:?}",
result.orphan_declarations
);
assert!(result.typo_matches.is_empty());
}
#[test]
fn test_val_orphan_still_detected() {
let fsti_content = r#"
module Test
val foo: int -> int
val missing_impl: int -> int
"#;
let fst_content = r#"
module Test
let foo x = x + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "missing_impl"),
"Val without implementation should still be flagged as orphan"
);
}
#[test]
fn test_assume_val_not_orphan() {
let fsti_content = r#"
module Test
assume val external_fn: int -> int
val foo: int -> int
"#;
let fst_content = r#"
module Test
let foo x = x + 1
"#;
let result = analyze_interface_consistency(fsti_content, fst_content);
assert!(
!result
.orphan_declarations
.iter()
.any(|(name, _, _)| name == "external_fn"),
"assume val should NOT be flagged as orphan"
);
}
#[test]
fn test_content_hash_identical() {
let content = "module Test\n\nval foo: int -> int\n";
let hash1 = compute_content_hash(content);
let hash2 = compute_content_hash(content);
assert_eq!(hash1, hash2, "Same content should produce same hash");
}
#[test]
fn test_content_hash_whitespace_insensitive() {
let content1 = "module Test\n\nval foo: int -> int\n";
let content2 = "module Test\n\n\n val foo: int -> int\n";
let hash1 = compute_content_hash(content1);
let hash2 = compute_content_hash(content2);
assert_eq!(hash1, hash2, "Same non-ws content should produce same hash");
}
#[test]
fn test_content_hash_detects_missing_content() {
let content1 = "module Test\n\nval foo: int -> int\nval bar: int -> int\n";
let content2 = "module Test\n\nval foo: int -> int\n";
let hash1 = compute_content_hash(content1);
let hash2 = compute_content_hash(content2);
assert_ne!(hash1, hash2, "Missing content should produce different hash");
}
#[test]
fn test_content_hash_detects_duplicated_content() {
let content1 = "module Test\n\nval foo: int -> int\n";
let content2 = "module Test\n\nval foo: int -> int\nval foo: int -> int\n";
let hash1 = compute_content_hash(content1);
let hash2 = compute_content_hash(content2);
assert_ne!(hash1, hash2, "Duplicated content should produce different hash");
}
#[test]
fn test_extract_declaration_names() {
let content = r#"
module Test
val foo: int -> int
type mytype = int
let bar x = x + 1
"#;
let names = extract_all_declaration_names(content);
assert!(names.contains(&"foo".to_string()));
assert!(names.contains(&"mytype".to_string()));
assert!(names.contains(&"bar".to_string()));
}
#[test]
fn test_count_declaration_names() {
let names = vec!["foo".to_string(), "bar".to_string(), "foo".to_string()];
let counts = count_declaration_names(&names);
assert_eq!(counts.get("foo"), Some(&2));
assert_eq!(counts.get("bar"), Some(&1));
}
#[test]
fn test_validate_reorder_identical_content() {
let content = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let warnings = validate_reorder(content, content, "Test.fsti");
assert!(warnings.is_empty(), "Identical content should produce no warnings");
}
#[test]
fn test_validate_reorder_detects_lost_content() {
let original = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let reordered = r#"module Test
val foo: int -> int
"#;
let warnings = validate_reorder(original, reordered, "Test.fsti");
assert!(!warnings.is_empty(), "Lost content should produce warnings");
assert!(
warnings.iter().any(|w| w.severity == ReorderWarningSeverity::Critical),
"Lost content should produce CRITICAL warning. Got: {:?}",
warnings
);
}
#[test]
fn test_validate_reorder_detects_lost_module() {
let original = r#"module Test
val foo: int -> int
"#;
let reordered = r#"
val foo: int -> int
"#;
let warnings = validate_reorder(original, reordered, "Test.fsti");
assert!(
warnings.iter().any(|w| w.severity == ReorderWarningSeverity::Critical),
"Lost module should produce CRITICAL warning. Got: {:?}",
warnings
);
}
#[test]
fn test_validate_reorder_allows_declaration_reordering() {
let original = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let reordered = r#"module Test
val bar: int -> int
val foo: int -> int
"#;
let warnings = validate_reorder(original, reordered, "Test.fsti");
let critical_count = warnings
.iter()
.filter(|w| w.severity == ReorderWarningSeverity::Critical)
.count();
assert_eq!(
critical_count, 0,
"Reordering should not cause critical warnings. Got: {:?}",
warnings
);
}
#[test]
fn test_preflight_check_basic() {
let content = r#"module Test
val foo: int -> int
type mytype = int
"#;
let result = preflight_reorder_check(content);
assert!(result.is_ok(), "Valid content should pass preflight");
let report = result.unwrap();
assert!(report.has_module_declaration);
assert_eq!(report.total_declarations, 2);
assert!(report.declaration_names.contains(&"foo".to_string()));
assert!(report.declaration_names.contains(&"mytype".to_string()));
}
#[test]
fn test_preflight_check_no_module() {
let content = r#"
val foo: int -> int
"#;
let result = preflight_reorder_check(content);
assert!(result.is_err(), "Missing module should fail preflight");
}
#[test]
fn test_dry_run_no_changes() {
let fst_content = r#"
module Test
let foo x = x + 1
let bar x = foo x
"#;
let fsti_content = r#"
module Test
val foo: int -> int
val bar: int -> int
"#;
let result = dry_run_reorder(fsti_content, fst_content);
assert!(result.is_ok());
let dry_run = result.unwrap();
assert!(!dry_run.has_changes, "No forward refs means no changes needed");
assert_eq!(dry_run.warning_level, DryRunWarningLevel::None);
}
#[test]
fn test_dry_run_with_changes() {
let fst_content = r#"module Test
type mytype = int
let foo (x: mytype) = x
"#;
let fsti_content = r#"module Test
val foo: mytype -> mytype
type mytype = int
"#;
let result = dry_run_reorder(fsti_content, fst_content);
if let Ok(dry_run) = result {
if dry_run.has_changes {
assert!(!dry_run.movements.is_empty(), "If changes, should have movements");
}
}
}
#[test]
fn test_dry_run_warning_levels() {
let dry_run_low = DryRunResult {
movements: vec![
MovementInfo {
name: "a".to_string(),
old_position: 0,
new_position: 1,
direction: MovementDirection::Down,
distance: 1,
},
],
total_declarations: 5,
has_changes: true,
warning_level: DryRunWarningLevel::Low,
validation_warnings: vec![],
};
assert_eq!(dry_run_low.warning_level, DryRunWarningLevel::Low);
}
#[test]
fn test_movement_direction() {
let up = MovementInfo {
name: "test".to_string(),
old_position: 5,
new_position: 2,
direction: MovementDirection::Up,
distance: 3,
};
assert_eq!(up.direction, MovementDirection::Up);
let down = MovementInfo {
name: "test".to_string(),
old_position: 2,
new_position: 5,
direction: MovementDirection::Down,
distance: 3,
};
assert_eq!(down.direction, MovementDirection::Down);
}
#[test]
fn test_validation_blocks_dangerous_fix() {
let original = r#"module Test
val foo: int -> int
val bar: int -> int
"#;
let corrupted = r#"module Test
val foo: int -> int
"#;
let warnings = validate_reorder(original, corrupted, "Test.fsti");
let has_critical = warnings
.iter()
.any(|w| w.severity == ReorderWarningSeverity::Critical);
assert!(has_critical, "Corrupted content should produce critical warnings");
}
#[test]
fn test_reorder_preserves_content_basic() {
let fst_content = r#"module Test
type mytype = int
let foo (x: mytype) = x
"#;
let fsti_content = r#"module Test
val foo: mytype -> mytype
type mytype = int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
if let Ok((reordered, _, changed)) = result {
if changed {
let warnings = validate_reorder(fsti_content, &reordered, "Test.fsti");
println!("Original:\n{}", fsti_content);
println!("Reordered:\n{}", reordered);
println!("Warnings: {:?}", warnings);
let orig_names = extract_all_declaration_names(fsti_content);
let new_names = extract_all_declaration_names(&reordered);
assert_eq!(
orig_names.len(),
new_names.len(),
"Declaration count should be preserved"
);
}
}
}
#[test]
fn test_mutual_recursion_preservation() {
let fst_content = r#"
module Test
type a = A of b
and b = B of a
let foo x = x
"#;
let fsti_content = r#"
module Test
type a = A of b
and b = B of a
val foo: int -> int
"#;
let result = reorder_fsti_content(fsti_content, fst_content);
assert!(result.is_ok());
let (reordered, _, _) = result.unwrap();
assert!(reordered.contains("type a"), "Type a should be preserved");
assert!(reordered.contains("and b"), "Mutual recursion 'and b' should be preserved");
let warnings = validate_reorder(fsti_content, &reordered, "Test.fsti");
let critical_count = warnings
.iter()
.filter(|w| w.severity == ReorderWarningSeverity::Critical)
.count();
assert_eq!(critical_count, 0, "Mutual recursion should be handled correctly");
}
}