Skip to content

Commit 1cbd9a3

Browse files
authored
Merge pull request #12 from OxfordAbstracts/better-errors-and-caching
Better errors and caching
2 parents 0d189cc + 9f8ea8a commit 1cbd9a3

9 files changed

Lines changed: 1462 additions & 156 deletions

File tree

src/build/cache.rs

Lines changed: 481 additions & 4 deletions
Large diffs are not rendered by default.

src/build/mod.rs

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ fn build_from_sources_impl(
622622
effective_timeout.map(|t| t.as_secs()).unwrap_or(0));
623623

624624
let mut done = 0usize;
625-
let mut rebuilt_set: HashSet<String> = HashSet::new();
625+
let mut export_diffs: HashMap<String, cache::ExportDiff> = HashMap::new();
626626
let mut cached_count = 0usize;
627627

628628
for level in &levels {
@@ -635,7 +635,7 @@ fn build_from_sources_impl(
635635
{
636636
let pm = &parsed[idx];
637637
if let Some(ref mut cache) = cache {
638-
if !cache.needs_rebuild(&pm.module_name, pm.source_hash, &rebuilt_set) {
638+
if !cache.needs_rebuild_smart(&pm.module_name, pm.source_hash, &export_diffs) {
639639
if let Some(exports) = cache.get_exports(&pm.module_name) {
640640
done += 1;
641641
cached_count += 1;
@@ -647,6 +647,7 @@ fn build_from_sources_impl(
647647
module_results.push(ModuleResult {
648648
path: pm.path.clone(),
649649
module_name: pm.module_name.clone(),
650+
650651
type_errors: vec![],
651652
cached: true,
652653
});
@@ -705,24 +706,71 @@ fn build_from_sources_impl(
705706
" [{}/{}] ok: {} ({:.2?})",
706707
done, total_modules, pm.module_name, elapsed
707708
);
708-
let import_names: Vec<String> = pm.import_parts.iter()
709-
.map(|parts| interner::resolve_module_name(parts))
710-
.collect();
711-
let exports_changed = if let Some(ref mut c) = cache {
712-
c.update(pm.module_name.clone(), pm.source_hash, result.exports.clone(), import_names)
709+
let has_errors = !result.errors.is_empty();
710+
if has_errors {
711+
// Don't cache modules with type errors
712+
// Compute a full diff so downstream modules rebuild
713+
let old_exports = cache.as_mut().and_then(|c| c.get_exports(&pm.module_name).cloned());
714+
if let Some(ref mut c) = cache {
715+
c.remove(&pm.module_name);
716+
}
717+
// Treat error modules as having all exports changed
718+
let diff = if let Some(old) = old_exports {
719+
cache::ExportDiff::compute(&old, &result.exports)
720+
} else {
721+
// New module with errors — force downstream rebuild
722+
let mut d = cache::ExportDiff::default();
723+
d.instances_changed = true;
724+
d
725+
};
726+
if !diff.is_empty() {
727+
log::debug!(
728+
"[build-plan] {} exports changed: values={:?}, types={:?}, classes={:?}, instances={}, operators={}",
729+
pm.module_name, diff.changed_values, diff.changed_types, diff.changed_classes,
730+
diff.instances_changed, diff.operators_changed
731+
);
732+
export_diffs.insert(pm.module_name.clone(), diff);
733+
}
713734
} else {
714-
true
715-
};
716-
// Only add to rebuilt_set if exports actually changed
717-
if exports_changed {
718-
rebuilt_set.insert(pm.module_name.clone());
735+
let import_names: Vec<String> = pm.import_parts.iter()
736+
.map(|parts| interner::resolve_module_name(parts))
737+
.collect();
738+
let module_ref = pm.module.as_ref().unwrap();
739+
let import_items = cache::extract_import_items(&module_ref.imports);
740+
// Get old exports before updating for diff computation
741+
let old_exports = cache.as_mut().and_then(|c| c.get_exports(&pm.module_name).cloned());
742+
let exports_changed = if let Some(ref mut c) = cache {
743+
c.update(pm.module_name.clone(), pm.source_hash, result.exports.clone(), import_names, import_items)
744+
} else {
745+
true
746+
};
747+
// Compute per-symbol diff for smart rebuild
748+
if exports_changed {
749+
let diff = if let Some(old) = old_exports {
750+
cache::ExportDiff::compute(&old, &result.exports)
751+
} else {
752+
// New module — force downstream rebuild
753+
let mut d = cache::ExportDiff::default();
754+
d.instances_changed = true;
755+
d
756+
};
757+
if !diff.is_empty() {
758+
log::debug!(
759+
"[build-plan] {} exports changed: values={:?}, types={:?}, classes={:?}, instances={}, operators={}",
760+
pm.module_name, diff.changed_values, diff.changed_types, diff.changed_classes,
761+
diff.instances_changed, diff.operators_changed
762+
);
763+
export_diffs.insert(pm.module_name.clone(), diff);
764+
}
765+
}
719766
}
720767
// Register exports immediately — result.exports is moved,
721768
// then result (with its types HashMap) is dropped.
722769
registry.register(&pm.module_parts, result.exports);
723770
module_results.push(ModuleResult {
724771
path: pm.path.clone(),
725772
module_name: pm.module_name.clone(),
773+
726774
type_errors: result.errors,
727775
cached: false,
728776
});
@@ -746,7 +794,7 @@ fn build_from_sources_impl(
746794
for &idx in level.iter() {
747795
let pm = &parsed[idx];
748796
if let Some(ref mut cache) = cache {
749-
if !cache.needs_rebuild(&pm.module_name, pm.source_hash, &rebuilt_set) {
797+
if !cache.needs_rebuild_smart(&pm.module_name, pm.source_hash, &export_diffs) {
750798
if let Some(exports) = cache.get_exports(&pm.module_name) {
751799
done += 1;
752800
cached_count += 1;
@@ -832,21 +880,63 @@ fn build_from_sources_impl(
832880
" [{}/{}] ok: {} ({:.2?})",
833881
done, total_modules, pm.module_name, elapsed
834882
);
835-
let import_names: Vec<String> = pm.import_parts.iter()
836-
.map(|parts| interner::resolve_module_name(parts))
837-
.collect();
838-
let exports_changed = if let Some(ref mut c) = cache {
839-
c.update(pm.module_name.clone(), pm.source_hash, result.exports.clone(), import_names)
883+
let has_errors = !result.errors.is_empty();
884+
if has_errors {
885+
// Don't cache modules with type errors
886+
let old_exports = cache.as_mut().and_then(|c| c.get_exports(&pm.module_name).cloned());
887+
if let Some(ref mut c) = cache {
888+
c.remove(&pm.module_name);
889+
}
890+
let diff = if let Some(old) = old_exports {
891+
cache::ExportDiff::compute(&old, &result.exports)
892+
} else {
893+
let mut d = cache::ExportDiff::default();
894+
d.instances_changed = true;
895+
d
896+
};
897+
if !diff.is_empty() {
898+
log::debug!(
899+
"[build-plan] {} exports changed: values={:?}, types={:?}, classes={:?}, instances={}, operators={}",
900+
pm.module_name, diff.changed_values, diff.changed_types, diff.changed_classes,
901+
diff.instances_changed, diff.operators_changed
902+
);
903+
export_diffs.insert(pm.module_name.clone(), diff);
904+
}
840905
} else {
841-
true
842-
};
843-
if exports_changed {
844-
rebuilt_set.insert(pm.module_name.clone());
906+
let import_names: Vec<String> = pm.import_parts.iter()
907+
.map(|parts| interner::resolve_module_name(parts))
908+
.collect();
909+
let module_ref = pm.module.as_ref().unwrap();
910+
let import_items = cache::extract_import_items(&module_ref.imports);
911+
let old_exports = cache.as_mut().and_then(|c| c.get_exports(&pm.module_name).cloned());
912+
let exports_changed = if let Some(ref mut c) = cache {
913+
c.update(pm.module_name.clone(), pm.source_hash, result.exports.clone(), import_names, import_items)
914+
} else {
915+
true
916+
};
917+
if exports_changed {
918+
let diff = if let Some(old) = old_exports {
919+
cache::ExportDiff::compute(&old, &result.exports)
920+
} else {
921+
let mut d = cache::ExportDiff::default();
922+
d.instances_changed = true;
923+
d
924+
};
925+
if !diff.is_empty() {
926+
log::debug!(
927+
"[build-plan] {} exports changed: values={:?}, types={:?}, classes={:?}, instances={}, operators={}",
928+
pm.module_name, diff.changed_values, diff.changed_types, diff.changed_classes,
929+
diff.instances_changed, diff.operators_changed
930+
);
931+
export_diffs.insert(pm.module_name.clone(), diff);
932+
}
933+
}
845934
}
846935
registry.register(&pm.module_parts, result.exports);
847936
module_results.push(ModuleResult {
848937
path: pm.path.clone(),
849938
module_name: pm.module_name.clone(),
939+
850940
type_errors: result.errors,
851941
cached: false,
852942
});

src/build/portable.rs

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Uses a deduplicated string table so each symbol is stored once.
44
//! Symbol references are u32 indices into the string table.
55
6-
use std::collections::{HashMap, HashSet};
6+
use std::collections::{BTreeMap, BTreeSet, HashMap};
77

88
use serde::{Deserialize, Serialize};
99

@@ -64,7 +64,7 @@ impl StringTableReader {
6464

6565
// ===== Portable QualifiedIdent =====
6666

67-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
67+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
6868
pub struct PQI {
6969
pub module: Option<u32>,
7070
pub name: u32,
@@ -227,34 +227,34 @@ fn rest_role(p: &PRole) -> Role {
227227

228228
#[derive(Serialize, Deserialize, Clone, Debug)]
229229
pub struct PModuleExports {
230-
pub values: HashMap<PQI, PScheme>,
231-
pub class_methods: HashMap<PQI, (PQI, Vec<PQI>)>,
232-
pub data_constructors: HashMap<PQI, Vec<PQI>>,
233-
pub ctor_details: HashMap<PQI, (PQI, Vec<PQI>, Vec<PType>)>,
234-
pub instances: HashMap<PQI, Vec<(Vec<PType>, Vec<(PQI, Vec<PType>)>)>>,
235-
pub type_operators: HashMap<PQI, PQI>,
236-
pub value_fixities: HashMap<PQI, (PAssociativity, u8)>,
237-
pub type_fixities: HashMap<PQI, (PAssociativity, u8)>,
238-
pub function_op_aliases: HashSet<PQI>,
239-
pub value_operator_targets: HashMap<PQI, PQI>,
240-
pub constrained_class_methods: HashSet<PQI>,
241-
pub type_aliases: HashMap<PQI, (Vec<PQI>, PType)>,
242-
pub class_param_counts: HashMap<PQI, usize>,
243-
pub value_origins: HashMap<u32, u32>,
244-
pub type_origins: HashMap<u32, u32>,
245-
pub class_origins: HashMap<u32, u32>,
246-
pub operator_class_targets: HashMap<u32, u32>,
247-
pub class_fundeps: HashMap<u32, (Vec<u32>, Vec<(Vec<usize>, Vec<usize>)>)>,
248-
pub type_con_arities: HashMap<PQI, usize>,
249-
pub type_roles: HashMap<u32, Vec<PRole>>,
250-
pub newtype_names: HashSet<u32>,
251-
pub signature_constraints: HashMap<PQI, Vec<(PQI, Vec<PType>)>>,
252-
pub type_kinds: HashMap<u32, PType>,
253-
pub class_type_kinds: HashMap<u32, PType>,
254-
pub partial_dischargers: HashSet<u32>,
255-
pub self_referential_aliases: HashSet<u32>,
256-
pub class_superclasses: HashMap<PQI, (Vec<u32>, Vec<(PQI, Vec<PType>)>)>,
257-
pub method_own_constraints: HashMap<PQI, Vec<u32>>,
230+
pub values: BTreeMap<PQI, PScheme>,
231+
pub class_methods: BTreeMap<PQI, (PQI, Vec<PQI>)>,
232+
pub data_constructors: BTreeMap<PQI, Vec<PQI>>,
233+
pub ctor_details: BTreeMap<PQI, (PQI, Vec<PQI>, Vec<PType>)>,
234+
pub instances: BTreeMap<PQI, Vec<(Vec<PType>, Vec<(PQI, Vec<PType>)>)>>,
235+
pub type_operators: BTreeMap<PQI, PQI>,
236+
pub value_fixities: BTreeMap<PQI, (PAssociativity, u8)>,
237+
pub type_fixities: BTreeMap<PQI, (PAssociativity, u8)>,
238+
pub function_op_aliases: BTreeSet<PQI>,
239+
pub value_operator_targets: BTreeMap<PQI, PQI>,
240+
pub constrained_class_methods: BTreeSet<PQI>,
241+
pub type_aliases: BTreeMap<PQI, (Vec<PQI>, PType)>,
242+
pub class_param_counts: BTreeMap<PQI, usize>,
243+
pub value_origins: BTreeMap<u32, u32>,
244+
pub type_origins: BTreeMap<u32, u32>,
245+
pub class_origins: BTreeMap<u32, u32>,
246+
pub operator_class_targets: BTreeMap<u32, u32>,
247+
pub class_fundeps: BTreeMap<u32, (Vec<u32>, Vec<(Vec<usize>, Vec<usize>)>)>,
248+
pub type_con_arities: BTreeMap<PQI, usize>,
249+
pub type_roles: BTreeMap<u32, Vec<PRole>>,
250+
pub newtype_names: BTreeSet<u32>,
251+
pub signature_constraints: BTreeMap<PQI, Vec<(PQI, Vec<PType>)>>,
252+
pub type_kinds: BTreeMap<u32, PType>,
253+
pub class_type_kinds: BTreeMap<u32, PType>,
254+
pub partial_dischargers: BTreeSet<u32>,
255+
pub self_referential_aliases: BTreeSet<u32>,
256+
pub class_superclasses: BTreeMap<PQI, (Vec<u32>, Vec<(PQI, Vec<PType>)>)>,
257+
pub method_own_constraints: BTreeMap<PQI, Vec<u32>>,
258258
}
259259

260260
impl PModuleExports {

src/lsp/handlers/diagnostics.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tower_lsp::lsp_types::*;
44

55
use crate::cst::Module;
66
use crate::interner;
7-
use crate::build::cache::ModuleCache;
7+
use crate::build::cache::{ModuleCache, extract_import_items};
88
use crate::typechecker::registry::ModuleRegistry;
99

1010
use super::super::{Backend, FileState};
@@ -112,8 +112,9 @@ impl Backend {
112112
let import_names: Vec<String> = module.imports.iter()
113113
.map(|imp| interner::resolve_module_name(&imp.module.parts))
114114
.collect();
115+
let import_items = extract_import_items(&module.imports);
115116
let mut cache = self.module_cache.write().await;
116-
cache.update(module_name.clone(), source_hash, check_result.exports, import_names);
117+
cache.update(module_name.clone(), source_hash, check_result.exports, import_names, import_items);
117118
drop(cache);
118119

119120
// Publish diagnostics for the changed module
@@ -124,9 +125,27 @@ impl Backend {
124125

125126
self.info(format!("[on_change] total: {:.2?}", on_change_start.elapsed())).await;
126127
}
128+
129+
/// Re-typecheck all files that are currently open in the editor.
130+
/// Called after initialization completes to process files opened during loading.
131+
pub(crate) async fn typecheck_open_files(&self) {
132+
let open_files: Vec<(String, String)> = {
133+
let files = self.files.read().await;
134+
files.iter().map(|(uri, fs)| (uri.clone(), fs.source.clone())).collect()
135+
};
136+
if open_files.is_empty() {
137+
return;
138+
}
139+
self.info(format!("[lsp] typechecking {} open file(s) after init", open_files.len())).await;
140+
for (uri_str, source) in open_files {
141+
if let Ok(uri) = Url::parse(&uri_str) {
142+
self.on_change(uri, source).await;
143+
}
144+
}
145+
}
127146
}
128147

129-
fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], source: &str) -> Vec<Diagnostic> {
148+
pub(crate) fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], source: &str) -> Vec<Diagnostic> {
130149
errors
131150
.iter()
132151
.map(|err| {
@@ -149,14 +168,14 @@ fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], s
149168
severity: Some(DiagnosticSeverity::ERROR),
150169
code: Some(NumberOrString::String(format!("TypeError.{}", err.code()))),
151170
source: Some("pfc".to_string()),
152-
message: format!("{err}"),
171+
message: format!("{}\n", err.format_pretty()),
153172
..Default::default()
154173
}
155174
})
156175
.collect()
157176
}
158177

159-
fn error_to_range(err: &crate::diagnostics::CompilerError, source: &str) -> Range {
178+
pub(crate) fn error_to_range(err: &crate::diagnostics::CompilerError, source: &str) -> Range {
160179
match err.get_span() {
161180
Some(span) => match span.to_pos(source) {
162181
Some((start, end)) => Range {

0 commit comments

Comments
 (0)