From 9d365a8a57c7533ef87568bff748dae9a719e553 Mon Sep 17 00:00:00 2001 From: Ayaz Hafiz Date: Sat, 20 Aug 2022 15:14:59 -0500 Subject: [PATCH] Support basic diagnostic reporting --- Cargo.lock | 46 +++++++ Cargo.toml | 1 + crates/compiler/exhaustive/src/lib.rs | 7 + crates/compiler/problem/src/can.rs | 47 +++++++ crates/compiler/solve/src/solve.rs | 2 +- crates/lang_srv/Cargo.toml | 22 ++++ crates/lang_srv/src/convert.rs | 176 ++++++++++++++++++++++++++ crates/lang_srv/src/registry.rs | 158 +++++++++++++++++++++++ crates/lang_srv/src/server.rs | 112 ++++++++++++++++ crates/reporting/src/error/type.rs | 2 +- crates/reporting/src/report.rs | 2 +- 11 files changed, 572 insertions(+), 3 deletions(-) create mode 100644 crates/lang_srv/Cargo.toml create mode 100644 crates/lang_srv/src/convert.rs create mode 100644 crates/lang_srv/src/registry.rs create mode 100644 crates/lang_srv/src/server.rs diff --git a/Cargo.lock b/Cargo.lock index 6b0997dd70..b2389452b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "async-trait" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -169,6 +180,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto_impl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -3304,6 +3327,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3847,6 +3881,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -4063,6 +4108,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ef6d36a044..579db508d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "crates/wasi-libc-sys", "crates/wasm_module", "crates/wasm_interp", + "crates/lang_srv", ] exclude = [ diff --git a/crates/compiler/exhaustive/src/lib.rs b/crates/compiler/exhaustive/src/lib.rs index 3434f384f8..741408847f 100644 --- a/crates/compiler/exhaustive/src/lib.rs +++ b/crates/compiler/exhaustive/src/lib.rs @@ -160,6 +160,13 @@ impl Error { Error::Unmatchable { .. } => Warning, } } + + pub fn region(&self) -> Region { + match self { + Error::Incomplete(region, _, _) => *region, + Error::Redundant { branch_region, .. } => *branch_region, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index e6904133e5..bd51f6bcc9 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -445,6 +445,14 @@ pub enum PrecedenceProblem { BothNonAssociative(Region, Loc, Loc), } +impl PrecedenceProblem { + pub fn region(&self) -> Region { + match self { + PrecedenceProblem::BothNonAssociative(region, _, _) => *region, + } + } +} + /// Enum to store the various types of errors that can cause parsing an integer to fail. #[derive(Debug, Clone, PartialEq, Eq)] pub enum IntErrorKind { @@ -619,6 +627,45 @@ impl RuntimeError { err => format!("{err:?}"), } } + + pub fn region(&self) -> Region { + match self { + RuntimeError::Shadowing { shadow, .. } => shadow.region, + RuntimeError::InvalidOptionalValue { field_region, .. } => *field_region, + RuntimeError::UnsupportedPattern(region) + | RuntimeError::MalformedPattern(_, region) + | RuntimeError::OpaqueOutsideScope { + referenced_region: region, + .. + } + | RuntimeError::OpaqueAppliedToMultipleArgs(region) + | RuntimeError::ValueNotExposed { region, .. } + | RuntimeError::ModuleNotImported { region, .. } + | RuntimeError::InvalidPrecedence(_, region) + | RuntimeError::MalformedIdentifier(_, _, region) + | RuntimeError::MalformedTypeName(_, region) + | RuntimeError::MalformedClosure(region) + | RuntimeError::InvalidRecordUpdate { region } + | RuntimeError::InvalidFloat(_, region, _) + | RuntimeError::InvalidInt(_, _, region, _) + | RuntimeError::EmptySingleQuote(region) + | RuntimeError::MultipleCharsInSingleQuote(region) + | RuntimeError::DegenerateBranch(region) + | RuntimeError::InvalidInterpolation(region) + | RuntimeError::InvalidHexadecimal(region) + | RuntimeError::InvalidUnicodeCodePt(region) => *region, + RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => Region::zero(), + RuntimeError::LookupNotInScope(ident, _) => ident.region, + RuntimeError::OpaqueNotDefined { usage, .. } => usage.region, + RuntimeError::OpaqueNotApplied(ident) => ident.region, + RuntimeError::CircularDef(cycle) => cycle[0].symbol_region, + RuntimeError::NonExhaustivePattern => Region::zero(), + RuntimeError::NoImplementationNamed { .. } + | RuntimeError::NoImplementation + | RuntimeError::VoidValue + | RuntimeError::ExposedButNotDefined(_) => Region::zero(), + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/compiler/solve/src/solve.rs b/crates/compiler/solve/src/solve.rs index f5ede38e6b..95dafa4851 100644 --- a/crates/compiler/solve/src/solve.rs +++ b/crates/compiler/solve/src/solve.rs @@ -667,7 +667,7 @@ fn solve( } } None => { - problems.push(TypeError::UnexposedLookup(*symbol)); + problems.push(TypeError::UnexposedLookup(*region, *symbol)); state } diff --git a/crates/lang_srv/Cargo.toml b/crates/lang_srv/Cargo.toml new file mode 100644 index 0000000000..4d81e71912 --- /dev/null +++ b/crates/lang_srv/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "roc_lang_srv" +version = "0.0.1" +edition = "2021" + +[[bin]] +name = "roc_ls" +path = "src/server.rs" + +[dependencies] +roc_load = { path = "../compiler/load" } +roc_problem = { path = "../compiler/problem" } +roc_region = { path = "../compiler/region" } +roc_reporting = { path = "../reporting" } +roc_solve_problem = { path = "../compiler/solve_problem" } +roc_target = { path = "../compiler/roc_target" } + +bumpalo = { version = "3.8.0", features = ["collections"] } + +tower-lsp = "0.17.0" +tokio = { version = "1.20.1", features = [ "rt", "rt-multi-thread", "macros", "io-std", ] } +parking_lot = "0.12.1" diff --git a/crates/lang_srv/src/convert.rs b/crates/lang_srv/src/convert.rs new file mode 100644 index 0000000000..b7d2dae891 --- /dev/null +++ b/crates/lang_srv/src/convert.rs @@ -0,0 +1,176 @@ +use roc_region::all::{LineColumnRegion, LineInfo, Region}; +use tower_lsp::lsp_types::{Position, Range}; + +fn range_of_region(line_info: &LineInfo, region: Region) -> Range { + let LineColumnRegion { start, end } = line_info.convert_region(region); + Range { + start: Position { + line: start.line, + character: start.column, + }, + end: Position { + line: end.line, + character: end.column, + }, + } +} + +pub(crate) mod diag { + use std::path::Path; + + use roc_load::LoadingProblem; + use roc_region::all::LineInfo; + use roc_solve_problem::TypeError; + + use roc_reporting::report::{RocDocAllocator, Severity}; + use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + + use super::range_of_region; + + pub trait IntoLspSeverity { + fn into_lsp_severity(self) -> DiagnosticSeverity; + } + + impl IntoLspSeverity for Severity { + fn into_lsp_severity(self) -> DiagnosticSeverity { + match self { + Severity::RuntimeError => DiagnosticSeverity::ERROR, + Severity::Warning => DiagnosticSeverity::WARNING, + } + } + } + + pub trait IntoLspDiagnostic<'a> { + type Feed; + + fn into_lsp_diagnostic(self, feed: &'a Self::Feed) -> Option; + } + + impl IntoLspDiagnostic<'_> for LoadingProblem<'_> { + type Feed = (); + + fn into_lsp_diagnostic(self, _feed: &()) -> Option { + let range = Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 1, + }, + }; + + let msg; + match self { + LoadingProblem::FileProblem { filename, error } => { + msg = format!( + "Failed to load {} due to an I/O error: {}", + filename.display(), + error + ); + } + LoadingProblem::ParsingFailed(_) => { + unreachable!("should be formatted before sent back") + } + LoadingProblem::UnexpectedHeader(header) => { + msg = format!("Unexpected header: {}", header); + } + LoadingProblem::MsgChannelDied => { + msg = format!("Internal error: message channel died"); + } + LoadingProblem::ErrJoiningWorkerThreads => { + msg = format!("Internal error: analysis worker threads died"); + } + LoadingProblem::TriedToImportAppModule => { + msg = format!("Attempted to import app module"); + } + LoadingProblem::FormattedReport(report) => { + msg = report; + } + }; + + Some(Diagnostic { + range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("load".to_owned()), + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } + + pub struct ProblemFmt<'a> { + pub alloc: &'a RocDocAllocator<'a>, + pub line_info: &'a LineInfo, + pub path: &'a Path, + } + + impl<'a> IntoLspDiagnostic<'a> for roc_problem::can::Problem { + type Feed = ProblemFmt<'a>; + + fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option { + let range = range_of_region(fmt.line_info, self.region()); + + let report = roc_reporting::report::can_problem( + &fmt.alloc, + &fmt.line_info, + fmt.path.to_path_buf(), + self, + ); + + let severity = report.severity.into_lsp_severity(); + + let mut msg = String::new(); + report.render_ci(&mut msg, fmt.alloc); + + Some(Diagnostic { + range, + severity: Some(severity), + code: None, + code_description: None, + source: None, + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } + + impl<'a> IntoLspDiagnostic<'a> for TypeError { + type Feed = ProblemFmt<'a>; + + fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option { + let range = range_of_region(fmt.line_info, self.region()); + + let report = roc_reporting::report::type_problem( + &fmt.alloc, + &fmt.line_info, + fmt.path.to_path_buf(), + self, + )?; + + let severity = report.severity.into_lsp_severity(); + + let mut msg = String::new(); + report.render_ci(&mut msg, fmt.alloc); + + Some(Diagnostic { + range, + severity: Some(severity), + code: None, + code_description: None, + source: None, + message: msg, + related_information: None, + tags: None, + data: None, + }) + } + } +} diff --git a/crates/lang_srv/src/registry.rs b/crates/lang_srv/src/registry.rs new file mode 100644 index 0000000000..b5da1f5b67 --- /dev/null +++ b/crates/lang_srv/src/registry.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use bumpalo::Bump; +use roc_load::{LoadedModule, LoadingProblem}; +use roc_region::all::LineInfo; +use roc_reporting::report::RocDocAllocator; +use tower_lsp::lsp_types::{Diagnostic, Url}; + +use crate::convert::diag::{IntoLspDiagnostic, ProblemFmt}; + +pub(crate) enum DocumentChange { + Modified(Url, String), + Closed(Url), +} + +#[derive(Debug)] +struct Document { + url: Url, + source: String, + + arena: Bump, + + // Incrementally updated module, diagnostis, etc. + module: Option>, + diagnostics: Option>, +} + +impl Document { + fn new(url: Url, source: String) -> Self { + Self { + url, + source, + arena: Bump::new(), + + module: None, + diagnostics: None, + } + } + + fn prime(&mut self, source: String) { + self.source = source; + self.module = None; + self.diagnostics = None; + } + + fn module(&mut self) -> Result<&mut LoadedModule, LoadingProblem<'_>> { + if let Some(Ok(module)) = &mut self.module { + // Safety: returning for time self is alive + return Ok(unsafe { std::mem::transmute(module) }); + } + + let fi = self.url.to_file_path().unwrap(); + let src_dir = fi.parent().unwrap().to_path_buf(); + + let loaded = roc_load::load_and_typecheck_str( + &self.arena, + fi, + &self.source, + src_dir, + Default::default(), + roc_target::TargetInfo::default_x86_64(), + roc_reporting::report::RenderTarget::Generic, + ); + + match loaded { + Ok(module) => { + self.module = Some(Ok(module)); + Ok(self.module.as_mut().unwrap().as_mut().unwrap()) + } + Err(problem) => { + self.module = Some(Err(())); + Err(problem) + } + } + } + + fn diagnostics(&mut self) -> Vec { + if let Some(diagnostics) = &self.diagnostics { + return diagnostics.clone(); + } + + let loaded: Result<&'static mut LoadedModule, LoadingProblem> = + unsafe { std::mem::transmute(self.module()) }; + + let diagnostics = match loaded { + Ok(module) => { + let lines: Vec<_> = self.source.lines().collect(); + let line_info = LineInfo::new(&self.source); + + let alloc = RocDocAllocator::new(&lines, module.module_id, &module.interns); + + let mut all_problems = Vec::new(); + let module_path = self.url.to_file_path().unwrap(); + let fmt = ProblemFmt { + alloc: &alloc, + line_info: &line_info, + path: &module_path, + }; + + for can_problem in module + .can_problems + .remove(&module.module_id) + .unwrap_or_default() + { + if let Some(diag) = can_problem.into_lsp_diagnostic(&fmt) { + all_problems.push(diag); + } + } + + for type_problem in module + .type_problems + .remove(&module.module_id) + .unwrap_or_default() + { + if let Some(diag) = type_problem.into_lsp_diagnostic(&fmt) { + all_problems.push(diag); + } + } + + all_problems + } + Err(problem) => { + let mut all_problems = vec![]; + all_problems.extend(problem.into_lsp_diagnostic(&())); + all_problems + } + }; + + self.diagnostics = Some(diagnostics); + self.diagnostics.as_ref().unwrap().clone() + } +} + +#[derive(Debug, Default)] +pub(crate) struct Registry { + documents: HashMap, +} + +impl Registry { + pub fn apply_change(&mut self, change: DocumentChange) { + match change { + DocumentChange::Modified(url, source) => match self.documents.get_mut(&url) { + Some(document) => document.prime(source), + None => { + self.documents + .insert(url.clone(), Document::new(url, source)); + } + }, + DocumentChange::Closed(url) => { + self.documents.remove(&url); + } + } + } + + pub fn diagnostics(&mut self, document: &Url) -> Vec { + self.documents.get_mut(document).unwrap().diagnostics() + } +} diff --git a/crates/lang_srv/src/server.rs b/crates/lang_srv/src/server.rs new file mode 100644 index 0000000000..6c6a617ebb --- /dev/null +++ b/crates/lang_srv/src/server.rs @@ -0,0 +1,112 @@ +use parking_lot::{Mutex, MutexGuard}; +use registry::{DocumentChange, Registry}; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +mod convert; +mod registry; + +#[derive(Debug)] +struct RocLs { + client: Client, + registry: Mutex, +} + +impl RocLs { + pub fn new(client: Client) -> Self { + Self { + client, + registry: Mutex::new(Registry::default()), + } + } + + fn registry(&self) -> MutexGuard { + self.registry.lock() + } + + pub fn capabilities() -> ServerCapabilities { + let text_document_sync = Some(TextDocumentSyncCapability::Options( + // TODO: later on make this incremental + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + ..TextDocumentSyncOptions::default() + }, + )); + let hover_provider = Some(HoverProviderCapability::Simple(true)); + + ServerCapabilities { + text_document_sync, + hover_provider, + ..ServerCapabilities::default() + } + } + + /// Records a document content change. + async fn change(&self, fi: Url, text: String, version: i32) { + self.registry() + .apply_change(DocumentChange::Modified(fi.clone(), text)); + + let diagnostics = self.registry().diagnostics(&fi); + self.client + .publish_diagnostics(fi, diagnostics, Some(version)) + .await; + } + + async fn close(&self, fi: Url) { + self.registry().apply_change(DocumentChange::Closed(fi)); + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for RocLs { + async fn initialize(&self, _: InitializeParams) -> Result { + Ok(InitializeResult { + capabilities: Self::capabilities(), + ..InitializeResult::default() + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "Roc language server initialized.") + .await; + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let TextDocumentItem { + uri, text, version, .. + } = params.text_document; + self.change(uri, text, version).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let VersionedTextDocumentIdentifier { uri, version, .. } = params.text_document; + + // NOTE: We specify that we expect full-content syncs in the server capabilities, + // so here we assume the only change passed is a change of the entire document's content. + let TextDocumentContentChangeEvent { text, .. } = + params.content_changes.into_iter().next().unwrap(); + + self.change(uri, text, version).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let TextDocumentIdentifier { uri } = params.text_document; + self.close(uri).await; + } + + async fn shutdown(&self) -> Result<()> { + Ok(()) + } +} + +#[tokio::main] +async fn main() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + + let (service, socket) = LspService::new(RocLs::new); + Server::new(stdin, stdout, socket).serve(service).await; +} diff --git a/crates/reporting/src/error/type.rs b/crates/reporting/src/error/type.rs index 6992db99c6..b3a7e4676d 100644 --- a/crates/reporting/src/error/type.rs +++ b/crates/reporting/src/error/type.rs @@ -71,7 +71,7 @@ pub fn type_problem<'b>( symbol, overall_type, )), - UnexposedLookup(symbol) => { + UnexposedLookup(_, symbol) => { let title = "UNRECOGNIZED NAME".to_string(); let doc = alloc .stack(vec![alloc diff --git a/crates/reporting/src/report.rs b/crates/reporting/src/report.rs index 58c1841656..386c0d5100 100644 --- a/crates/reporting/src/report.rs +++ b/crates/reporting/src/report.rs @@ -134,7 +134,7 @@ impl<'b> Report<'b> { } /// Render to CI console output, where no colors are available. - pub fn render_ci(self, buf: &'b mut String, alloc: &'b RocDocAllocator<'b>) { + pub fn render_ci(self, buf: &mut String, alloc: &'b RocDocAllocator<'b>) { let err_msg = ""; self.pretty(alloc)