use crate::frontend::ast::{Expr, ExprKind};
use crate::frontend::parser::Parser;
use std::collections::HashMap;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, Diagnostic, DiagnosticSeverity, Documentation, Hover,
HoverContents, Location, MarkedString, NumberOrString, Position, Range,
};
pub struct SemanticAnalyzer {
symbol_table: SymbolTable,
}
#[derive(Debug, Default)]
struct SymbolTable {
symbols: HashMap<String, Symbol>,
}
#[derive(Debug, Clone)]
struct Symbol {
name: String,
kind: String,
documentation: Option<String>,
}
impl SemanticAnalyzer {
pub fn new() -> Self {
Self {
symbol_table: SymbolTable::default(),
}
}
pub fn get_completions(
&self,
_document: &str,
_position: Position,
) -> Result<Vec<CompletionItem>> {
let keywords = vec![
"fun", "let", "if", "else", "match", "struct", "trait", "impl", "actor", "import",
"for", "while", "break", "continue", "true", "false",
];
let mut completions = Vec::new();
for keyword in keywords {
completions.push(CompletionItem {
label: keyword.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some(format!("Ruchy keyword: {keyword}")),
..Default::default()
});
}
let types = vec!["i32", "i64", "f32", "f64", "String", "bool", "()"];
for type_name in types {
completions.push(CompletionItem {
label: type_name.to_string(),
kind: Some(CompletionItemKind::TYPE_PARAMETER),
detail: Some(format!("Built-in type: {type_name}")),
..Default::default()
});
}
for symbol in self.symbol_table.symbols.values() {
completions.push(CompletionItem {
label: symbol.name.clone(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some(symbol.kind.clone()),
documentation: symbol
.documentation
.as_ref()
.map(|doc| Documentation::String(doc.clone())),
..Default::default()
});
}
Ok(completions)
}
pub fn get_hover_info(&self, document: &str, _position: Position) -> Result<Option<Hover>> {
let mut parser = Parser::new(document);
if parser.parse().is_err() {
return Ok(None);
}
let hover_text = "Ruchy Language\n\nA systems scripting language with:\n- Strong type inference\n- Actor-based concurrency\n- DataFrame-first collections\n- Rust interoperability";
Ok(Some(Hover {
contents: HoverContents::Scalar(MarkedString::String(hover_text.to_string())),
range: None,
}))
}
pub fn get_definition(&self, _document: &str, _position: Position) -> Result<Option<Location>> {
Ok(None)
}
pub fn get_diagnostics(&mut self, document: &str) -> Result<Vec<Diagnostic>> {
let mut diagnostics = Vec::new();
let mut parser = Parser::new(document);
match parser.parse() {
Ok(ast) => {
self.update_symbol_table(&ast, document);
}
Err(parse_error) => {
diagnostics.push(Diagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 10,
},
},
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("parse_error".to_string())),
message: format!("Parse error: {parse_error}"),
source: Some("ruchy".to_string()),
..Default::default()
});
}
}
Ok(diagnostics)
}
fn update_symbol_table(&mut self, ast: &Expr, document: &str) {
self.extract_symbols(ast, document);
}
fn extract_symbols(&mut self, expr: &Expr, _document: &str) {
match &expr.kind {
ExprKind::Function { name, params, .. } => {
let symbol = Symbol {
name: name.clone(),
kind: "function".to_string(),
documentation: None,
};
self.symbol_table.symbols.insert(name.clone(), symbol);
for param in params {
let param_symbol = Symbol {
name: param.name(),
kind: "parameter".to_string(),
documentation: None,
};
self.symbol_table.symbols.insert(param.name(), param_symbol);
}
}
ExprKind::Let { name, .. } => {
let symbol = Symbol {
name: name.clone(),
kind: "variable".to_string(),
documentation: None,
};
self.symbol_table.symbols.insert(name.clone(), symbol);
}
ExprKind::Struct { name, .. } => {
let symbol = Symbol {
name: name.clone(),
kind: "struct".to_string(),
documentation: None,
};
self.symbol_table.symbols.insert(name.clone(), symbol);
}
_ => {
}
}
}
}
impl Default for SemanticAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod property_tests_analyzer {
use proptest::prelude::*;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_semantic_analyzer_new() {
let analyzer = SemanticAnalyzer::new();
assert!(analyzer.symbol_table.symbols.is_empty());
}
#[test]
fn test_semantic_analyzer_default() {
let analyzer = SemanticAnalyzer::default();
assert!(analyzer.symbol_table.symbols.is_empty());
}
#[test]
fn test_get_completions_includes_keywords() {
let analyzer = SemanticAnalyzer::new();
let completions = analyzer
.get_completions(
"",
Position {
line: 0,
character: 0,
},
)
.expect("should succeed");
let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(labels.contains(&"fun"));
assert!(labels.contains(&"let"));
assert!(labels.contains(&"if"));
}
#[test]
fn test_get_completions_includes_types() {
let analyzer = SemanticAnalyzer::new();
let completions = analyzer
.get_completions(
"",
Position {
line: 0,
character: 0,
},
)
.expect("should succeed");
let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(labels.contains(&"i32"));
assert!(labels.contains(&"String"));
assert!(labels.contains(&"bool"));
}
#[test]
fn test_get_hover_info_returns_some() {
let analyzer = SemanticAnalyzer::new();
let hover = analyzer
.get_hover_info(
"let x = 42",
Position {
line: 0,
character: 0,
},
)
.expect("should succeed");
assert!(hover.is_some());
}
#[test]
fn test_get_definition_returns_none() {
let analyzer = SemanticAnalyzer::new();
let location = analyzer
.get_definition(
"let x = 42",
Position {
line: 0,
character: 0,
},
)
.expect("should succeed");
assert!(location.is_none());
}
#[test]
fn test_get_diagnostics_valid_code() {
let mut analyzer = SemanticAnalyzer::new();
let diagnostics = analyzer
.get_diagnostics("let x = 42")
.expect("should succeed");
assert!(diagnostics.is_empty());
}
#[test]
fn test_symbol_table_default() {
let table = SymbolTable::default();
assert!(table.symbols.is_empty());
}
#[test]
fn test_symbol_clone() {
let symbol = Symbol {
name: "test".to_string(),
kind: "variable".to_string(),
documentation: Some("doc".to_string()),
};
let cloned = symbol.clone();
assert_eq!(cloned.name, "test");
assert_eq!(cloned.kind, "variable");
assert_eq!(cloned.documentation, Some("doc".to_string()));
}
#[test]
fn test_get_diagnostics_invalid_code() {
let mut analyzer = SemanticAnalyzer::new();
let diagnostics = analyzer
.get_diagnostics("fun { incomplete")
.expect("should succeed");
assert!(!diagnostics.is_empty());
}
}