#![cfg_attr(coverage_nightly, coverage(off))]
#[cfg(feature = "ruchy-ast")]
use crate::services::context::AstItem;
#[cfg(feature = "ruchy-ast")]
#[derive(Debug, Clone)]
pub struct RuchyMlAstExtractor {
items: Vec<AstItem>,
current_module: Option<String>,
complexity: u32,
actor_complexity: u32,
pattern_complexity: u32,
proof_complexity: u32,
}
#[cfg(feature = "ruchy-ast")]
impl Default for RuchyMlAstExtractor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "ruchy-ast")]
impl RuchyMlAstExtractor {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self {
items: Vec::new(),
current_module: None,
complexity: 0,
actor_complexity: 0,
pattern_complexity: 0,
proof_complexity: 0,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn analyze_ruchy_source(mut self, source: &str) -> Result<Vec<AstItem>, String> {
if source.trim().is_empty() {
return Ok(vec![]);
}
if !self.is_valid_ruchy_syntax(source) {
return Err("Invalid Ruchy syntax".to_string());
}
for (line_num, line) in source.lines().enumerate() {
self.process_line(line, line_num + 1)?;
}
Ok(self.items)
}
fn process_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") {
return Ok(());
}
if trimmed.starts_with("module ") {
self.extract_module_from_line(trimmed, line_number)?;
}
else if trimmed.starts_with("let ") && trimmed.contains('(') {
self.extract_function_from_line(trimmed, line_number)?;
}
else if trimmed.starts_with("type ") {
self.extract_type_from_line(trimmed, line_number)?;
}
else if trimmed.starts_with("actor ") {
self.extract_actor_from_line(trimmed, line_number)?;
}
else if trimmed.starts_with("theorem ") {
self.extract_theorem_from_line(trimmed, line_number)?;
}
else if trimmed.starts_with("match ") {
self.pattern_complexity += 1;
}
else if trimmed.starts_with('|') && trimmed.contains("->") {
self.pattern_complexity += 1;
}
else if trimmed.starts_with("proof ") {
self.proof_complexity += 5;
}
else if trimmed.starts_with("message ") && self.current_module.is_some() {
self.actor_complexity += 1;
}
Ok(())
}
fn is_valid_ruchy_syntax(&self, source: &str) -> bool {
!source.contains("{{{ !!!") && !source.contains("INVALID")
}
fn extract_module_from_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[1].to_string();
self.current_module = Some(name.clone());
self.items.push(AstItem::Struct {
name,
visibility: "public".to_string(),
fields_count: 0,
derives: vec![],
line: line_number,
});
}
Ok(())
}
fn extract_function_from_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
if let Some(start) = line.find("let ") {
if let Some(paren) = line.find('(') {
let name_part = &line[start + 4..paren];
let name = name_part.trim().to_string();
let qualified_name = self.qualify_name(&name);
let _complexity = self.calculate_basic_complexity(line);
self.items.push(AstItem::Function {
name: qualified_name,
visibility: "public".to_string(),
is_async: false,
line: line_number,
});
}
}
Ok(())
}
fn extract_type_from_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[1]
.split('<')
.next()
.and_then(|s| s.split('=').next())
.map_or_else(|| parts[1].to_string(), |s| s.trim().to_string());
let qualified_name = self.qualify_name(&name);
self.items.push(AstItem::Struct {
name: qualified_name,
visibility: "public".to_string(),
fields_count: 0,
derives: vec![],
line: line_number,
});
}
Ok(())
}
fn extract_actor_from_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[1].trim_end_matches('{').to_string();
let qualified_name = self.qualify_name(&name);
self.actor_complexity = 3;
self.items.push(AstItem::Struct {
name: qualified_name,
visibility: "public".to_string(),
fields_count: 0, derives: vec![],
line: line_number,
});
}
Ok(())
}
fn extract_theorem_from_line(&mut self, line: &str, line_number: usize) -> Result<(), String> {
if let Some(colon) = line.find(':') {
let name_part = &line[7..colon]; let name = name_part.trim().to_string();
let qualified_name = self.qualify_name(&name);
self.proof_complexity = 5;
self.items.push(AstItem::Function {
name: qualified_name,
visibility: "public".to_string(),
is_async: false,
line: line_number,
});
}
Ok(())
}
fn qualify_name(&self, name: &str) -> String {
if let Some(ref module) = self.current_module {
if !name.contains("::") {
return format!("{module}::{name}");
}
}
name.to_string()
}
fn calculate_basic_complexity(&self, _line: &str) -> u32 {
1
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn analyze_pattern_complexity(&mut self, source: &str) -> Result<u32, String> {
self.pattern_complexity = 0;
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("match ") {
self.pattern_complexity += 1;
} else if trimmed.starts_with('|') && trimmed.contains("->") {
self.pattern_complexity += 1;
} else if trimmed.contains("if ") && trimmed.contains("->") {
self.pattern_complexity += 2;
}
}
Ok(self.pattern_complexity)
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn get_total_complexity(&self) -> u32 {
self.complexity + self.actor_complexity + self.pattern_complexity + self.proof_complexity
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_ml_style_function() {
let source = "let add(x, y) = x + y";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 1);
if let AstItem::Function { name, .. } = &items[0] {
assert_eq!(name, "add");
} else {
panic!("Expected Function variant");
}
}
#[test]
fn test_extract_type_with_refinement() {
let source = "type PositiveInt = { x: Int | x > 0 }";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 1);
if let AstItem::Struct { name, .. } = &items[0] {
assert_eq!(name, "PositiveInt");
} else {
panic!("Expected Struct variant for type");
}
}
#[test]
fn test_extract_actor_definition() {
let source = "actor Counter {\n state count = 0\n}";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 1);
if let AstItem::Struct { name, .. } = &items[0] {
assert_eq!(name, "Counter");
} else {
panic!("Expected Struct variant for actor");
}
}
#[test]
fn test_module_qualified_names() {
let source = "module Math\n\nlet square(x) = x * x";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 2);
if let AstItem::Struct { name, .. } = &items[0] {
assert_eq!(name, "Math");
}
if let AstItem::Function { name, .. } = &items[1] {
assert_eq!(name, "Math::square");
}
}
#[test]
fn test_pattern_matching_complexity() {
let source = r#"
let fibonacci(n) =
match n with
| 0 -> 0
| 1 -> 1
| n -> fibonacci(n - 1) + fibonacci(n - 2)
"#;
let mut extractor = RuchyMlAstExtractor::new();
let complexity = extractor.analyze_pattern_complexity(source).unwrap();
assert_eq!(complexity, 4); }
#[test]
fn test_theorem_extraction() {
let source = "theorem sum_commutative: forall a b: Int, a + b = b + a";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 1);
if let AstItem::Function { name, .. } = &items[0] {
assert_eq!(name, "sum_commutative");
} else {
panic!("Expected Function variant for theorem");
}
}
#[test]
fn test_empty_source() {
let source = "";
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(source).unwrap();
assert_eq!(items.len(), 0);
}
#[test]
fn test_invalid_syntax() {
let source = "{{{ !!! INVALID";
let extractor = RuchyMlAstExtractor::new();
let result = extractor.analyze_ruchy_source(source);
assert!(result.is_err());
}
}
#[cfg(test)]
#[cfg(feature = "ruchy-ast")]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn ml_extraction_never_panics(s in ".*") {
let extractor = RuchyMlAstExtractor::new();
let _ = extractor.analyze_ruchy_source(&s);
}
#[test]
fn empty_strings_produce_empty_ast(s in "\\s*") {
let extractor = RuchyMlAstExtractor::new();
let result = extractor.analyze_ruchy_source(&s);
prop_assert!(result.unwrap().is_empty());
}
#[test]
fn function_names_preserved(
name in "[a-z][a-zA-Z0-9_]*",
params in "[a-z, ]*"
) {
let source = format!("let {}({}) = 42", name, params);
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(&source).unwrap();
if !items.is_empty() {
if let AstItem::Function { name: fn_name, .. } = &items[0] {
prop_assert_eq!(fn_name, &name);
}
}
}
#[test]
fn type_names_preserved(name in "[A-Z][a-zA-Z0-9]*") {
let source = format!("type {} = Int", name);
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(&source).unwrap();
if !items.is_empty() {
if let AstItem::Struct { name: type_name, .. } = &items[0] {
prop_assert_eq!(type_name, &name);
}
}
}
#[test]
fn actor_complexity_always_three(name in "[A-Z][a-zA-Z0-9]*") {
let source = format!("actor {} {{}}", name);
let extractor = RuchyMlAstExtractor::new();
let items = extractor.analyze_ruchy_source(&source).unwrap();
if !items.is_empty() {
match &items[0] {
AstItem::Struct { .. } => {},
_ => prop_assert!(false, "Expected Struct variant for actor"),
}
}
}
}
}