Skip to content

Commit 494dbea

Browse files
committed
address dual eval concerns
1 parent cd04e4e commit 494dbea

File tree

2 files changed

+79
-35
lines changed

2 files changed

+79
-35
lines changed

crates/pcb-zen-core/src/lang/eval.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ pub struct EvalOutput {
9393
session: EvalSession,
9494
}
9595

96+
/// Output of `parse_and_analyze_file`, preserving both parsed AST and full eval output.
97+
#[derive(Clone)]
98+
pub struct ParseAndAnalyzeOutput {
99+
pub ast: AstModule,
100+
pub eval_output: EvalOutput,
101+
}
102+
96103
impl EvalOutput {
97104
/// Get the session (for creating a new EvalContext that shares state with this output).
98105
pub fn session(&self) -> &EvalSession {
@@ -1517,7 +1524,7 @@ impl EvalContext {
15171524
&self,
15181525
path: PathBuf,
15191526
contents: String,
1520-
) -> WithDiagnostics<Option<AstModule>> {
1527+
) -> WithDiagnostics<ParseAndAnalyzeOutput> {
15211528
self.session.clear_load_cache();
15221529
self.session.clear_symbol_maps(&path);
15231530

@@ -1615,7 +1622,10 @@ impl EvalContext {
16151622
.update_symbol_maps(path.clone(), symbol_index, symbol_params, symbol_meta);
16161623
}
16171624

1618-
result.map(|output| Some(output.ast))
1625+
result.map(|output| ParseAndAnalyzeOutput {
1626+
ast: output.ast.clone(),
1627+
eval_output: output,
1628+
})
16191629
}
16201630

16211631
/// Get the frozen module for a file if it has been evaluated

crates/pcb-zen/src/lsp/mod.rs

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ pub struct LspEvalContext {
4040
open_files: Arc<RwLock<HashMap<PathBuf, String>>>,
4141
netlist_subscriptions: Arc<RwLock<HashMap<PathBuf, HashMap<String, JsonValue>>>>,
4242
suppress_netlist_updates: Arc<RwLock<HashSet<PathBuf>>>,
43+
/// Per-file cache of the last successful eval output, populated during
44+
/// `parse_file_with_contents` so that `viewer/getState` and
45+
/// `on_save_diagnostics` can reuse it without a redundant full evaluation.
46+
last_eval_outputs: Arc<RwLock<HashMap<PathBuf, pcb_zen_core::EvalOutput>>>,
4347
custom_request_handler: Option<Arc<CustomRequestHandler>>,
4448
}
4549

@@ -162,6 +166,7 @@ impl Default for LspEvalContext {
162166
open_files,
163167
netlist_subscriptions: Arc::new(RwLock::new(HashMap::new())),
164168
suppress_netlist_updates: Arc::new(RwLock::new(HashSet::new())),
169+
last_eval_outputs: Arc::new(RwLock::new(HashMap::new())),
165170
custom_request_handler: None,
166171
}
167172
}
@@ -278,6 +283,21 @@ impl LspEvalContext {
278283
self.suppress_netlist_updates.write().unwrap().remove(&key)
279284
}
280285

286+
fn set_last_eval_output(&self, path: &Path, output: pcb_zen_core::EvalOutput) {
287+
let key = self.normalize_path(path);
288+
self.last_eval_outputs.write().unwrap().insert(key, output);
289+
}
290+
291+
fn get_last_eval_output(&self, path: &Path) -> Option<pcb_zen_core::EvalOutput> {
292+
let key = self.normalize_path(path);
293+
self.last_eval_outputs.read().unwrap().get(&key).cloned()
294+
}
295+
296+
fn clear_last_eval_output(&self, path: &Path) {
297+
let key = self.normalize_path(path);
298+
self.last_eval_outputs.write().unwrap().remove(&key);
299+
}
300+
281301
fn evaluate_with_inputs(
282302
&self,
283303
path_buf: &Path,
@@ -536,6 +556,7 @@ impl LspContext for LspEvalContext {
536556
if let Ok(canon) = self.file_provider.canonicalize(path) {
537557
self.inner.clear_file_contents(&canon);
538558
}
559+
self.clear_last_eval_output(path);
539560
self.maybe_invalidate_symbol_library(path);
540561
self.maybe_invalidate_resolution_cache(path);
541562
}
@@ -573,19 +594,9 @@ impl LspContext for LspEvalContext {
573594
_ => return vec![],
574595
};
575596

576-
// Full eval to get schematic (same pattern as evaluate_with_inputs)
577-
let config = self.config_for(path);
578-
let mut ctx = EvalContext::from_session_and_config(Default::default(), config)
579-
.set_source_path(path.to_path_buf());
580-
581-
// Use open-file contents if available so we simulate the buffer, not disk
582-
let contents = self.get_load_contents(uri).ok().flatten();
583-
if let Some(ref contents) = contents {
584-
ctx = ctx.set_source_contents(contents.clone());
585-
}
586-
587-
let eval_result = ctx.eval();
588-
let Some(eval_output) = eval_result.output else {
597+
// Reuse the eval result produced during save validation to avoid a
598+
// second full evaluation on the save path.
599+
let Some(eval_output) = self.get_last_eval_output(path) else {
589600
return vec![];
590601
};
591602
let Ok(schematic) = eval_output.to_schematic() else {
@@ -707,6 +718,17 @@ impl LspContext for LspEvalContext {
707718
let passes = self.create_lsp_diagnostic_passes(&workspace_root);
708719
result.diagnostics.apply_passes(&passes);
709720

721+
if let Some(parsed) = result.output.as_ref() {
722+
// Cache the eval output so viewer/getState and
723+
// on_save_diagnostics can reuse it without a redundant
724+
// full evaluation.
725+
self.set_last_eval_output(path, parsed.eval_output.clone());
726+
} else {
727+
// Don't let save diagnostics/viewer state reuse stale
728+
// output when the current parse/eval failed.
729+
self.clear_last_eval_output(path);
730+
}
731+
710732
// Convert diagnostics to LSP format
711733
let diagnostics = result
712734
.diagnostics
@@ -716,7 +738,7 @@ impl LspContext for LspEvalContext {
716738

717739
LspEvalResult {
718740
diagnostics,
719-
ast: result.output.flatten(),
741+
ast: result.output.map(|parsed| parsed.ast),
720742
}
721743
}
722744
_ => {
@@ -1009,26 +1031,38 @@ impl LspContext for LspEvalContext {
10091031
Ok(params) => {
10101032
let state_json: Option<JsonValue> = match &params.uri {
10111033
LspUrl::File(path_buf) => {
1012-
// Get contents from memory or disk
1013-
let maybe_contents = self.get_load_contents(&params.uri).ok().flatten();
1014-
1015-
// Evaluate the module
1016-
let config = self.config_for(path_buf);
1017-
let ctx =
1018-
EvalContext::from_session_and_config(Default::default(), config);
1019-
1020-
let eval_result = if let Some(contents) = maybe_contents {
1021-
ctx.set_source_path(path_buf.clone())
1022-
.set_source_contents(contents)
1023-
.eval()
1034+
// Try the cached eval output first (populated during
1035+
// parse_file_with_contents) so we can return the
1036+
// schematic without a redundant full evaluation.
1037+
if let Some(cached) = self.get_last_eval_output(path_buf) {
1038+
cached
1039+
.to_schematic()
1040+
.ok()
1041+
.and_then(|s| serde_json::to_value(&s).ok())
10241042
} else {
1025-
ctx.set_source_path(path_buf.clone()).eval()
1026-
};
1027-
1028-
eval_result
1029-
.output
1030-
.and_then(|fmv| fmv.to_schematic().ok())
1031-
.and_then(|schematic| serde_json::to_value(&schematic).ok())
1043+
// Fallback: evaluate from scratch using the
1044+
// shared session so loaded modules are cached.
1045+
let maybe_contents =
1046+
self.get_load_contents(&params.uri).ok().flatten();
1047+
let config = self.config_for(path_buf);
1048+
let ctx = EvalContext::from_session_and_config(
1049+
self.inner.session().clone(),
1050+
config,
1051+
);
1052+
1053+
let eval_result = if let Some(contents) = maybe_contents {
1054+
ctx.set_source_path(path_buf.clone())
1055+
.set_source_contents(contents)
1056+
.eval()
1057+
} else {
1058+
ctx.set_source_path(path_buf.clone()).eval()
1059+
};
1060+
1061+
eval_result
1062+
.output
1063+
.and_then(|fmv| fmv.to_schematic().ok())
1064+
.and_then(|schematic| serde_json::to_value(&schematic).ok())
1065+
}
10321066
}
10331067
_ => None,
10341068
};

0 commit comments

Comments
 (0)