Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ thiserror = "2.0.17"

ropey = "1.6.1"
miniscript = "12"
simplicityhl = "0.5.0-rc.0"
simplicityhl = { git = "https://github.com/gerau/SimplicityHL.git", rev = "cac2b47e8b8acfae2b0e189154ab29475c88d5c8", features = ["docs"] }
nom = "8.0.0"
lazy_static = "1.5.0"

Expand Down
181 changes: 126 additions & 55 deletions src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use ropey::Rope;
use serde_json::Value;
use simplicityhl::driver::DependencyGraph;
use simplicityhl::error::WithContent;
use simplicityhl::parse::ParseFromStrWithErrors;

use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
Expand All @@ -23,7 +26,7 @@ use tower_lsp_server::lsp_types::{
TextDocumentSyncOptions, TextDocumentSyncSaveOptions, Uri, WorkDoneProgressOptions,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
};
use tower_lsp_server::{Client, LanguageServer};
use tower_lsp_server::{Client, LanguageServer, UriExt};

use miniscript::iter::TreeLike;
use simplicityhl::{ast, error::RichError, parse};
Expand All @@ -32,9 +35,8 @@ use crate::completion::{self, CompletionProvider};
use crate::error::LspError;
use crate::function::Functions;
use crate::utils::{
create_signature_info, find_all_references, find_builtin_signature, find_function_call_context,
find_function_name_range, find_key_position, find_related_call, get_call_span,
get_comments_from_lines, offset_to_position, position_to_span, span_contains,
create_signature_info, find_builtin_signature, find_function_call_context, find_key_position,
get_call_span, get_comments_from_lines, offset_to_position, position_to_span, span_contains,
span_to_positions,
};

Expand Down Expand Up @@ -63,9 +65,21 @@ fn get_semantic_token_legend() -> SemanticTokensLegend {
}

#[derive(Debug)]
struct Document {
functions: Functions,
text: Rope,
pub struct SourceFile {
pub uri: Uri,
pub text: Rope,
}

#[derive(Debug)]
pub struct Document {
/// Functions defined in file and imported modules.
pub functions: Functions,

/// Mapping from module id to it's Uri.
pub linearization_map: Vec<SourceFile>,

/// Source of given document.
pub text: Rope,
}

#[derive(Debug)]
Expand Down Expand Up @@ -209,7 +223,7 @@ impl LanguageServer for Backend {

for func in &functions {
// Add function name token (declaration)
if let Ok(name_range) = find_function_name_range(func, &doc.text) {
if let Ok(name_range) = doc.find_function_name_range(func) {
let len = u32::try_from(func.name().as_inner().len()).map_err(LspError::from)?;
raw_tokens.push((
name_range.start.line,
Expand Down Expand Up @@ -335,12 +349,16 @@ impl LanguageServer for Backend {
let symbols: Vec<DocumentSymbol> = functions
.iter()
.filter_map(|func| {
if func.file_id() != 0 {
return None;
}

// Get the full function range
let (start, end) = span_to_positions(func.span(), &doc.text).ok()?;
let full_range = Range { start, end };

// Get the function name range for selection
let selection_range = find_function_name_range(func, &doc.text).ok()?;
let selection_range = doc.find_function_name_range(func).ok()?;

// Build parameters detail string
let params_str = func
Expand Down Expand Up @@ -482,12 +500,11 @@ impl LanguageServer for Backend {
let Some(doc) = documents.get(uri) else {
return Ok(None);
};
let functions = doc.functions.functions();

let token_pos = params.text_document_position_params.position;

let token_span = position_to_span(token_pos, &doc.text)?;
let Ok(Some(call)) = find_related_call(&functions, token_span) else {
let Ok(Some(call)) = doc.find_related_call(token_span) else {
return Ok(None);
};

Expand Down Expand Up @@ -564,14 +581,14 @@ impl LanguageServer for Backend {
let token_position = params.text_document_position_params.position;
let token_span = position_to_span(token_position, &doc.text)?;

let Ok(Some(call)) = find_related_call(&functions, token_span) else {
let Ok(Some(call)) = doc.find_related_call(token_span) else {
let Some(func) = functions
.iter()
.find(|func| span_contains(func.span(), &token_span))
else {
return Ok(None);
};
let range = find_function_name_range(func, &doc.text)?;
let range = doc.find_function_name_range(func)?;

if token_position <= range.end && token_position >= range.start {
return Ok(Some(GotoDefinitionResponse::from(Location::new(
Expand All @@ -591,9 +608,14 @@ impl LanguageServer for Backend {
"Function {func} is not found"
)))?;

let (start, end) = span_to_positions(function.as_ref(), &doc.text)?;
let Some(source_file) = doc.linearization_map.get(function.file_id()) else {
return Ok(None);
};

let (start, end) = span_to_positions(function.as_ref(), &source_file.text)?;

Ok(Some(GotoDefinitionResponse::from(Location::new(
uri.clone(),
source_file.uri.clone(),
Range::new(start, end),
))))
}
Expand All @@ -605,7 +627,6 @@ impl LanguageServer for Backend {
let documents = self.document_map.read().await;
let uri = &params.text_document_position.text_document.uri;

// Return None if document not found (e.g., file has parse errors)
let Some(doc) = documents.get(uri) else {
return Ok(None);
};
Expand All @@ -615,21 +636,14 @@ impl LanguageServer for Backend {

let token_span = position_to_span(token_position, &doc.text)?;

let call_name =
find_related_call(&functions, token_span)?.map(simplicityhl::parse::Call::name);
let call_name = doc
.find_related_call(token_span)?
.map(simplicityhl::parse::Call::name);

match call_name {
Some(parse::CallName::Custom(_)) | None => {}
Some(name) => {
return Ok(Some(
find_all_references(&doc.text, &functions, name)?
.iter()
.map(|range| Location {
range: *range,
uri: uri.clone(),
})
.collect(),
));
return Ok(Some(doc.find_all_references(name)?));
}
}

Expand All @@ -640,22 +654,19 @@ impl LanguageServer for Backend {
return Ok(None);
};

let range = find_function_name_range(func, &doc.text)?;
let range = doc.find_function_name_range(func)?;

if (token_position <= range.end && token_position >= range.start) || call_name.is_some() {
Ok(Some(
find_all_references(
&doc.text,
&functions,
&parse::CallName::Custom(func.name().clone()),
)?
.into_iter()
.chain(std::iter::once(range))
.map(|range| Location {
range,
uri: uri.clone(),
})
.collect(),
documents
.values()
.filter_map(|document| {
document
.find_all_references(&parse::CallName::Custom(func.name().clone()))
.ok()
})
.flatten()
.collect(),
))
} else {
Ok(None)
Expand All @@ -674,16 +685,18 @@ impl Backend {

/// Function which executed on change of file (`did_save`, `did_open` or `did_change` methods)
async fn on_change(&self, params: TextDocumentItem<'_>) {
let path = Path::new(params.uri.path().as_str());

// Check if this is a witness file
if std::path::Path::new(params.uri.path().as_str())
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("wit"))
{
self.on_change_witness(params).await;
return;
}

let (err, document) = parse_program(params.text);
let (err, document) = parse_program(params.text, path);
let rope = Rope::from_str(params.text);
let mut documents = self.document_map.write().await;
if let Some(doc) = document {
Expand All @@ -698,6 +711,12 @@ impl Backend {
return None;
};

// HACK: We ignoring MainReqiured error because right now we cannot parse file as a
// library
if matches!(err.error(), simplicityhl::error::Error::MainRequired) {
return None;
}

Some(Diagnostic::new_simple(
Range::new(start, end),
err.error().to_string(),
Expand All @@ -724,6 +743,7 @@ fn create_document(program: &simplicityhl::parse::Program, text: &str) -> Docume
let mut document = Document {
functions: Functions::new(),
text: Rope::from_str(text),
linearization_map: Vec::new(),
};

program
Expand Down Expand Up @@ -752,22 +772,73 @@ fn create_document(program: &simplicityhl::parse::Program, text: &str) -> Docume

/// Parse and analyze program using [`simplicityhl`] compiler and return an list of [`RichError`]
/// to use in diagnostics. Also creates a [`Document`] if parsing is successfull.
fn parse_program(text: &str) -> (Vec<RichError>, Option<Document>) {
let mut error_collector = simplicityhl::error::ErrorCollector::new(Arc::from(text));

let Some(program) = parse::Program::parse_from_str_with_errors(text, &mut error_collector)
fn parse_program(text: &str, path: &Path) -> (Vec<RichError>, Option<Document>) {
let mut error_collector = simplicityhl::error::ErrorCollector::new();
let text: Arc<str> = Arc::from(text);
let source_file = simplicityhl::source::SourceFile::new(path, Arc::clone(&text));
let Some(program) =
parse::Program::parse_from_str_with_errors(source_file.clone(), &mut error_collector)
else {
return (error_collector.get().to_vec(), None);
};

if let Err(err) = ast::Program::analyze(&program) {
error_collector.update([err]);
let mut document = create_document(&program, text.as_ref());

let Some(canon_root) = path
.parent()
.and_then(|p| simplicityhl::source::CanonPath::canonicalize(p).ok())
else {
return (error_collector.get().to_vec(), Some(document));
};

let dependencies = match simplicityhl::resolution::DependencyMapBuilder::new(canon_root).build()
{
Ok(deps) => deps,
Err(err) => {
error_collector.push(RichError::new(err, (0..0).into()));

return (error_collector.get().to_vec(), Some(document));
}
};

let Some(driver_program) = ({
let Ok(Some(graph)) = DependencyGraph::new(
source_file.clone(),
Arc::from(dependencies.clone()),
&program,
&mut error_collector,
) else {
return (error_collector.get().to_vec(), Some(document));
};

let paths = graph.modules();

document.linearization_map = paths
.iter()
.filter_map(|module| {
let uri = Uri::from_file_path(module.source.name().as_path())?;
Some(SourceFile {
uri,
text: Rope::from_str(module.source.content().as_ref()),
})
})
.collect();

let Ok(program) = graph.linearize_and_build(&mut error_collector) else {
return (error_collector.get().to_vec(), Some(document));
};
program
}) else {
return (error_collector.get().to_vec(), Some(document));
};

document.populate_visible_functions(&driver_program);

if let Err(err) = ast::Program::analyze(&driver_program).with_content(Arc::clone(&text)) {
error_collector.extend(source_file, [err]);
}

(
error_collector.get().to_vec(),
Some(create_document(&program, text)),
)
(error_collector.get().to_vec(), Some(document))
}

/// Validate a witness (.wit) file and return diagnostics.
Expand Down Expand Up @@ -855,15 +926,15 @@ mod tests {

#[test]
fn test_parse_program_valid() {
let (err, doc) = parse_program(sample_program());
let (err, doc) = parse_program(sample_program(), Path::new(""));
assert!(err.is_empty(), "Expected no parsing error");
let doc = doc.expect("Expected Some(Document)");
assert_eq!(doc.functions.map.len(), 2);
}

#[test]
fn test_parse_program_invalid_ast() {
let (err, doc) = parse_program(invalid_program_on_ast());
let (err, doc) = parse_program(invalid_program_on_ast(), Path::new(""));
assert!(
err.first()
.expect("program should produce an error")
Expand All @@ -876,7 +947,7 @@ mod tests {

#[test]
fn test_parse_program_invalid_parse() {
let (err, doc) = parse_program(invalid_program_on_parsing());
let (err, doc) = parse_program(invalid_program_on_parsing(), Path::new(""));
assert_eq!(
err.first()
.expect("program should produce an error")
Expand Down
Loading
Loading