Skip to content

Commit

Permalink
Refactor CompilerHost to separate loading and reading documents
Browse files Browse the repository at this point in the history
`CompilerHost` can now load a file, which loads it and all its imports
(recursively) into the list of documents. Reading a document now only
blocks on the `RwLock`, and never does I/O
  • Loading branch information
kylewlacy committed Jan 14, 2024
1 parent b0fb2c0 commit fce13a1
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 88 deletions.
9 changes: 6 additions & 3 deletions crates/brioche/src/brioche/script/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ use super::{specifier::BriocheModuleSpecifier, Project};

#[tracing::instrument(skip(brioche, project), err)]
pub async fn check(brioche: &Brioche, project: &Project) -> anyhow::Result<CheckResult> {
let specifier = BriocheModuleSpecifier::File {
path: project.local_path.join("project.bri"),
};

let module_loader = super::BriocheModuleLoader::new(brioche);
let compiler_host = super::compiler_host::BriocheCompilerHost::new(brioche.clone());
compiler_host.load_document(&specifier).await?;

let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
module_loader: Some(Rc::new(module_loader.clone())),
source_map_getter: Some(Box::new(module_loader.clone())),
Expand Down Expand Up @@ -46,9 +52,6 @@ pub async fn check(brioche: &Brioche, project: &Project) -> anyhow::Result<Check

tracing::info!(path = %project.local_path.display(), %main_module, ?export_key_name, "running function");

let specifier = BriocheModuleSpecifier::File {
path: project.local_path.join("project.bri"),
};
let files = serde_v8::to_v8(&mut js_scope, &[specifier])?;

let mut js_scope = deno_core::v8::TryCatch::new(&mut js_scope);
Expand Down
223 changes: 138 additions & 85 deletions crates/brioche/src/brioche/script/compiler_host.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
cell::RefCell,
collections::HashMap,
collections::{HashMap, HashSet},
rc::Rc,
sync::{Arc, RwLock},
};
Expand All @@ -9,9 +9,9 @@ use anyhow::Context as _;
use deno_core::OpState;
use tokio::io::AsyncReadExt as _;

use crate::brioche::Brioche;
use crate::brioche::{project::analyze::find_imports, Brioche};

use super::specifier::{BriocheImportSpecifier, BriocheModuleSpecifier};
use super::specifier::{self, resolve, BriocheImportSpecifier, BriocheModuleSpecifier};

#[derive(Clone)]
pub struct BriocheCompilerHost {
Expand All @@ -21,108 +21,161 @@ pub struct BriocheCompilerHost {

impl BriocheCompilerHost {
pub fn new(brioche: Brioche) -> Self {
let documents: HashMap<_, _> = specifier::runtime_specifiers_with_contents()
.map(|(specifier, contents)| {
let contents = std::str::from_utf8(&contents)
.map_err(|_| anyhow::anyhow!("invalid UTF-8 in runtime file: {specifier}"))
.unwrap();
let document = BriocheDocument {
contents: Arc::new(contents.to_owned()),
version: 0,
};
(specifier, document)
})
.collect();
Self {
brioche,
documents: Arc::new(RwLock::new(HashMap::new())),
documents: Arc::new(RwLock::new(documents)),
}
}

pub async fn update_document(&self, uri: &url::Url, contents: &str) -> anyhow::Result<()> {
let specifier: BriocheModuleSpecifier = uri.try_into()?;
pub async fn load_document(&self, specifier: &BriocheModuleSpecifier) -> anyhow::Result<()> {
let mut already_visited = HashSet::new();
let mut specifiers_to_load = vec![specifier.clone()];

let mut documents = self
.documents
.write()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
documents
.entry(specifier)
.and_modify(|doc| {
if doc.contents != contents {
doc.version += 1;
doc.contents = contents.to_owned();
while let Some(specifier) = specifiers_to_load.pop() {
if !already_visited.insert(specifier.clone()) {
continue;
}

let contents = {
let documents = self
.documents
.read()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
documents.get(&specifier).map(|doc| doc.contents.clone())
};

let contents = match contents {
Some(contents) => contents,
None => {
let Some(mut reader) =
super::specifier::read_specifier_contents(&specifier).await?
else {
tracing::warn!("failed to load document {specifier}");
return Ok(());
};
let mut contents = String::new();
reader.read_to_string(&mut contents).await?;

let mut documents = self
.documents
.write()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;

match documents.entry(specifier.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
entry.get().contents.clone()
}
std::collections::hash_map::Entry::Vacant(entry) => {
tracing::info!("loaded new document into compiler host: {specifier}");
let contents = Arc::new(contents);
entry.insert(BriocheDocument {
contents: contents.clone(),
version: 0,
});
contents
}
}
}
})
.or_insert_with(|| BriocheDocument {
contents: contents.to_owned(),
version: 0,
});
};

let import_specifiers = {
let parsed = biome_js_parser::parse(
&contents,
biome_js_syntax::JsFileSource::ts()
.with_module_kind(biome_js_syntax::ModuleKind::Module),
biome_js_parser::JsParserOptions::default(),
)
.cast::<biome_js_syntax::JsModule>()
.expect("failed to cast module");

let Some(parsed_module) = parsed.try_tree() else {
tracing::warn!("failed to parse module {specifier}");
return Ok(());
};

find_imports(&parsed_module, |_| "<unknown>").collect::<Vec<_>>()
};

for import_specifier in import_specifiers {
let import_specifier = match import_specifier {
Ok(import_specifier) => import_specifier,
Err(error) => {
tracing::warn!("error parsing import specifier: {error:#}");
continue;
}
};
let resolved = resolve(&self.brioche, &import_specifier, &specifier).await;
let resolved = match resolved {
Ok(resolved) => resolved,
Err(error) => {
tracing::warn!("error resolving import specifier: {error:#}");
continue;
}
};

specifiers_to_load.push(resolved.clone());
}
}

Ok(())
}

#[allow(dead_code)]
pub async fn read_document<R>(
&self,
specifier: BriocheModuleSpecifier,
f: impl FnOnce(&BriocheDocument) -> R,
) -> anyhow::Result<Option<R>> {
pub async fn update_document(&self, uri: &url::Url, contents: &str) -> anyhow::Result<()> {
let specifier: BriocheModuleSpecifier = uri.try_into()?;

{
let documents = self
let mut documents = self
.documents
.read()
.write()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
if let Some(document) = documents.get(&specifier) {
let result = f(document);
return Ok(Some(result));
}
documents
.entry(specifier.clone())
.and_modify(|doc| {
if *doc.contents != contents {
doc.version += 1;

let doc_contents = Arc::make_mut(&mut doc.contents);
*doc_contents = contents.to_owned();
}
})
.or_insert_with(|| {
tracing::info!("inserted new document into compiler host: {specifier}");
BriocheDocument {
contents: Arc::new(contents.to_owned()),
version: 0,
}
});
}

let Some(mut reader) = super::specifier::read_specifier_contents(&specifier).await? else {
return Ok(None);
};
self.load_document(&specifier).await?;

let mut contents = String::new();
reader.read_to_string(&mut contents).await?;

let mut documents = self
.documents
.write()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
let document = documents
.entry(specifier)
.or_insert_with(|| BriocheDocument {
contents: contents.to_owned(),
version: 0,
});

let result = f(document);
Ok(Some(result))
Ok(())
}

// TODO: Remove
pub fn read_document_sync<R>(
pub fn read_loaded_document<R>(
&self,
specifier: BriocheModuleSpecifier,
f: impl FnOnce(&BriocheDocument) -> R,
) -> anyhow::Result<Option<R>> {
{
let documents = self
.documents
.read()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
if let Some(document) = documents.get(&specifier) {
let result = f(document);
return Ok(Some(result));
}
}

let Some(mut reader) = super::specifier::read_specifier_contents_sync(&specifier)? else {
return Ok(None);
};

let mut contents = String::new();
reader.read_to_string(&mut contents)?;

let mut documents = self
let documents = self
.documents
.write()
.read()
.map_err(|_| anyhow::anyhow!("failed to acquire lock on documents"))?;
let document = documents
.entry(specifier)
.or_insert_with(|| BriocheDocument {
contents: contents.to_owned(),
version: 0,
});
let Some(document) = documents.get(&specifier) else {
return Ok(None);
};

let result = f(document);
Ok(Some(result))
Expand All @@ -131,7 +184,7 @@ impl BriocheCompilerHost {

#[derive(Debug, Clone)]
pub struct BriocheDocument {
contents: String,
contents: Arc<String>,
version: u64,
}

Expand Down Expand Up @@ -164,12 +217,12 @@ fn brioche_compiler_host_state(state: Rc<RefCell<OpState>>) -> anyhow::Result<Br
pub fn op_brioche_file_read(
state: Rc<RefCell<OpState>>,
path: &str,
) -> anyhow::Result<Option<String>> {
) -> anyhow::Result<Option<Arc<String>>> {
let compiler_host = brioche_compiler_host_state(state)?;

let specifier: BriocheModuleSpecifier = path.parse()?;

let contents = compiler_host.read_document_sync(specifier, |doc| doc.contents.clone())?;
let contents = compiler_host.read_loaded_document(specifier, |doc| doc.contents.clone())?;
Ok(contents)
}

Expand All @@ -179,7 +232,7 @@ pub fn op_brioche_file_exists(state: Rc<RefCell<OpState>>, path: &str) -> anyhow

let specifier: BriocheModuleSpecifier = path.parse()?;

let result = compiler_host.read_document_sync(specifier, |_| ())?;
let result = compiler_host.read_loaded_document(specifier, |_| ())?;
Ok(result.is_some())
}

Expand All @@ -192,7 +245,7 @@ pub fn op_brioche_file_version(

let specifier: BriocheModuleSpecifier = path.parse()?;

let version = compiler_host.read_document_sync(specifier, |doc| doc.version)?;
let version = compiler_host.read_loaded_document(specifier, |doc| doc.version)?;
Ok(version)
}

Expand Down
38 changes: 38 additions & 0 deletions crates/brioche/src/brioche/script/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use tower_lsp::{Client, LanguageServer};
use crate::brioche::script::compiler_host::{brioche_compiler_host, BriocheCompilerHost};
use crate::brioche::Brioche;

use super::specifier::BriocheModuleSpecifier;

pub struct BriocheLspServer {
compiler_host: BriocheCompilerHost,
client: Client,
Expand Down Expand Up @@ -88,6 +90,26 @@ impl LanguageServer for BriocheLspServer {

async fn did_open(&self, params: DidOpenTextDocumentParams) {
tracing::info!(uri = %params.text_document.uri, "did open");

let specifier = lsp_uri_to_module_specifier(&params.text_document.uri);
match specifier {
Ok(specifier) => {
let result = self.compiler_host.load_document(&specifier).await;
match result {
Ok(()) => {}
Err(error) => {
tracing::warn!("failed to load document {specifier}: {error:#}");
}
}
}
Err(error) => {
tracing::warn!(
"failed to parse URI {}: {error:#}",
params.text_document.uri
);
}
}

let diagnostics = self
.diagnostics(TextDocumentIdentifier {
uri: params.text_document.uri.clone(),
Expand Down Expand Up @@ -315,6 +337,22 @@ impl LanguageServer for BriocheLspServer {
// }
}

fn lsp_uri_to_module_specifier(uri: &url::Url) -> anyhow::Result<BriocheModuleSpecifier> {
if uri.scheme() == "file" {
let uri_string = uri.to_string();
if uri_string.ends_with(".bri.ts") {
let brioche_uri_string = uri_string
.strip_suffix(".ts")
.context("failed to truncate URI")?;
let specifier = brioche_uri_string.parse()?;
return Ok(specifier);
}
}

let specifier = BriocheModuleSpecifier::try_from(uri)?;
Ok(specifier)
}

const TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(10);

struct JsLspTask {
Expand Down

0 comments on commit fce13a1

Please sign in to comment.