#![allow(clippy::needless_pass_by_value)]
use std::path::{Path, PathBuf};
use lean_rs_worker_parent::{
LeanWorkerDeclarationTargetInfo, LeanWorkerDeclarationTargetResult, LeanWorkerElabOptions, LeanWorkerLocalInfo,
LeanWorkerModuleCacheStatus, LeanWorkerModuleQuery, LeanWorkerModuleQueryBatchItem,
LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryBatchResult, LeanWorkerModuleQueryCacheFacts,
LeanWorkerModuleQueryOutcome, LeanWorkerModuleQueryResult, LeanWorkerModuleQuerySelector,
LeanWorkerModuleQueryTimings, LeanWorkerModuleSourceSpan, LeanWorkerNameRef, LeanWorkerOutputBudgets,
LeanWorkerProofPositionSelector, LeanWorkerProofStateInfo, LeanWorkerProofStateResult, LeanWorkerRenderedInfo,
LeanWorkerSurroundingDeclarationResult, LeanWorkerTypeAtResult,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::broker::ProjectHint;
use crate::envelope::{Freshness, Response, RuntimeFacts};
use crate::error::{Result, ServerError};
use crate::projections::{Diagnostic, ElabFailure, Severity, project_failure};
use crate::tools::source_input::{header_imports, read_query_file, resolve_path};
use crate::tools::{ToolContext, session_imports};
const MAX_REFERENCES: usize = 1000;
const PROOF_STATE_DIAGNOSTICS_ID: &str = "diagnostics";
const PROOF_STATE_CONTEXT_ID: &str = "proof_state";
const PROOF_AGENT_PER_FIELD_BYTES: u32 = 4 * 1024;
const EXPERT_QUERY_PER_FIELD_BYTES: u32 = 8 * 1024;
const DEFAULT_TOTAL_BYTES: u32 = 64 * 1024;
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct SourceSpan {
pub start_line: u32,
pub start_column: u32,
pub end_line: u32,
pub end_column: u32,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct RenderedText {
pub value: String,
pub truncated: bool,
}
#[derive(Debug, Clone, Copy, Default, Serialize, JsonSchema)]
pub struct DiagnosticSummary {
pub errors: usize,
pub warnings: usize,
pub info: usize,
}
impl DiagnosticSummary {
fn from_diagnostics(diagnostics: &[Diagnostic]) -> Self {
let mut summary = Self::default();
for diagnostic in diagnostics {
let bucket = match diagnostic.severity {
Severity::Error => &mut summary.errors,
Severity::Warning => &mut summary.warnings,
Severity::Info => &mut summary.info,
};
*bucket = bucket.saturating_add(1);
}
summary
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct DiagnosticsBlock {
pub summary: DiagnosticSummary,
pub diagnostics: Vec<Diagnostic>,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ModuleQueryTimings {
pub header_import_micros: u64,
pub elaboration_micros: u64,
pub projection_micros: u64,
pub rendering_micros: u64,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ModuleQueryFacts {
pub cache_status: &'static str,
pub output_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_entry_count: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_approx_bytes: Option<u64>,
pub timings: ModuleQueryTimings,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct LocalInfo {
pub name: String,
pub binder_info: String,
pub type_str: RenderedText,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<RenderedText>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct DeclarationTargetInfo {
pub short_name: String,
pub declaration_name: String,
pub namespace_name: String,
pub declaration_kind: String,
pub declaration_span: SourceSpan,
pub name_span: SourceSpan,
pub body_span: SourceSpan,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DeclarationTargetProjection {
Target { info: DeclarationTargetInfo },
NotFound,
Ambiguous { candidates: Vec<DeclarationTargetInfo> },
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum SurroundingDeclarationProjection {
Declaration { info: DeclarationTargetInfo },
None,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ProofStateContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub declaration_name: Option<String>,
pub namespace_name: String,
pub goals_before: Vec<String>,
pub goals_after: Vec<String>,
pub locals: Vec<LocalInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_type: Option<RenderedText>,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ProofStateProjection {
State { info: Box<ProofStateContext> },
Unavailable { message: String },
Ambiguous { candidates: Vec<DeclarationTargetInfo> },
NeedsBuild { missing: Vec<String> },
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum TypeAtProjection {
Term {
expr: RenderedText,
type_str: RenderedText,
#[serde(skip_serializing_if = "Option::is_none")]
expected_type: Option<RenderedText>,
span: SourceSpan,
},
NoTerm,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ReferenceHit {
pub file: String,
pub line: u32,
pub column: u32,
pub end_line: u32,
pub end_column: u32,
pub kind: &'static str,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ReferencesProjection {
pub references: Vec<ReferenceHit>,
pub truncated: bool,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ProofStateRequest {
pub file: PathBuf,
pub declaration: String,
#[serde(default)]
pub proof_position: ProofPositionSelector,
#[serde(default)]
pub project: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProofPositionSelector {
#[default]
Default,
Index {
index: u32,
},
AfterText {
text: String,
#[serde(default)]
occurrence: Option<u32>,
},
}
pub(crate) fn worker_proof_position(position: ProofPositionSelector) -> LeanWorkerProofPositionSelector {
match position {
ProofPositionSelector::Default => LeanWorkerProofPositionSelector::Entry,
ProofPositionSelector::Index { index } => LeanWorkerProofPositionSelector::Index { index },
ProofPositionSelector::AfterText { text, occurrence } => {
LeanWorkerProofPositionSelector::AfterText { text, occurrence }
}
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct SelectorMessage {
pub id: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ProofStateResult {
Context {
diagnostics: DiagnosticsBlock,
#[serde(skip_serializing_if = "Option::is_none")]
declaration_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
namespace_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
goals_before: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
goals_after: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
locals: Vec<LocalInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
expected_type: Option<RenderedText>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
truncated: bool,
total_truncated: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
unavailable: Vec<SelectorMessage>,
#[serde(skip_serializing_if = "Vec::is_empty")]
needs_build: Vec<SelectorMessage>,
#[serde(skip_serializing_if = "Vec::is_empty")]
ambiguous: Vec<DeclarationTargetInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
budget_exceeded: Vec<SelectorMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
query_facts: Option<Box<ModuleQueryFacts>>,
},
HeaderParseFailed {
summary: DiagnosticSummary,
diagnostics: Vec<Diagnostic>,
truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
query_facts: Option<ModuleQueryFacts>,
},
Unsupported,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum BatchProjection {
Diagnostics(DiagnosticsBlock),
ProofState(ProofStateProjection),
TypeAt(TypeAtProjection),
References(ReferencesProjection),
DeclarationTarget(DeclarationTargetProjection),
SurroundingDeclaration(SurroundingDeclarationProjection),
}
pub async fn proof_state(ctx: &ToolContext, req: ProofStateRequest) -> Result<Response<ProofStateResult>> {
let hint = ProjectHint::from_request(req.project.clone());
let meta = ctx.broker.resolve_meta(&hint)?;
let selectors = vec![
LeanWorkerModuleQuerySelector::Diagnostics {
id: PROOF_STATE_DIAGNOSTICS_ID.to_owned(),
},
LeanWorkerModuleQuerySelector::ProofStateInDeclaration {
id: PROOF_STATE_CONTEXT_ID.to_owned(),
declaration: req.declaration,
position: worker_proof_position(req.proof_position),
locals_raw: false,
},
];
let budgets = proof_agent_budgets();
let run = match crate::diagnosis::classify_missing_olean(
run_module_query_batch(ctx, hint.clone(), &meta.canonical_root, &req.file, selectors, budgets).await,
)? {
crate::diagnosis::CallOutcome::Ready(run) => run,
crate::diagnosis::CallOutcome::NeedsBuild(err) => {
let imports = read_query_file(&meta.canonical_root, &req.file)
.map(|input| input.imports)
.unwrap_or_default();
return proof_state_needs_build_response(ctx, hint, imports, err).await;
}
};
let freshness = run.freshness.clone();
match run.outcome {
BatchQueryRun::Ready {
result,
facts,
missing_imports,
} => {
let query_facts = ctx.config.verbosity.is_full().then(|| project_query_facts(facts));
let mut diagnostics = DiagnosticsBlock {
summary: DiagnosticSummary::default(),
diagnostics: Vec::new(),
truncated: false,
};
let mut declaration_name = None;
let mut namespace_name = None;
let mut goals_before = Vec::new();
let mut goals_after = Vec::new();
let mut locals = Vec::new();
let mut expected_type = None;
let mut truncated = false;
let mut unavailable = Vec::new();
let mut needs_build = Vec::new();
let mut needs_build_missing: Vec<String> = Vec::new();
let mut ambiguous: Vec<DeclarationTargetInfo> = Vec::new();
let mut budget_exceeded = Vec::new();
for item in result.items {
match project_batch_item(item, None) {
ProjectedBatchItem::Ok { id, result } => match (id.as_str(), result) {
(PROOF_STATE_DIAGNOSTICS_ID, BatchProjection::Diagnostics(block)) => {
diagnostics = block;
}
(PROOF_STATE_CONTEXT_ID, BatchProjection::ProofState(value)) => match value {
ProofStateProjection::State { info } => {
let info = *info;
declaration_name = info.declaration_name;
namespace_name = Some(info.namespace_name);
goals_before = info.goals_before;
goals_after = info.goals_after;
locals = info.locals;
expected_type = info.expected_type;
truncated = info.truncated;
}
ProofStateProjection::Unavailable { message } => {
unavailable.push(SelectorMessage { id, message });
}
ProofStateProjection::NeedsBuild { missing } => {
let message = if missing.is_empty() {
"project environment is incomplete".to_owned()
} else {
format!("missing: {}", missing.join(", "))
};
needs_build_missing.extend(missing);
needs_build.push(SelectorMessage { id, message });
}
ProofStateProjection::Ambiguous { candidates } => {
ambiguous.extend(candidates);
}
},
_ => {}
},
ProjectedBatchItem::Unavailable { id, message } => {
unavailable.push(SelectorMessage { id, message });
}
ProjectedBatchItem::BudgetExceeded { id, message } => {
budget_exceeded.push(SelectorMessage { id, message });
}
}
}
let needs_build_cue = (!needs_build.is_empty()).then(|| needs_build_missing.clone());
let ambiguous_cue = competing_decls(&ambiguous);
let response = Response::ok(
ProofStateResult::Context {
diagnostics,
declaration_name,
namespace_name,
goals_before,
goals_after,
locals,
expected_type,
truncated,
total_truncated: result.total_truncated,
unavailable,
needs_build,
ambiguous,
budget_exceeded,
query_facts: query_facts.map(Box::new),
},
freshness,
)
.with_runtime(run.runtime.clone());
let response = match needs_build_cue {
Some(missing) => crate::diagnosis::warn_needs_build(
response,
&crate::diagnosis::IncompleteCause::MissingImports(missing),
),
None => response,
};
let response = crate::diagnosis::warn_ambiguous(response, &ambiguous_cue);
let response = match crate::diagnosis::execution_taint(&run.runtime) {
Some(event) => crate::diagnosis::warn_execution_taint(response, event),
None => response,
};
Ok(warn_session_missing_imports(response, &missing_imports))
}
BatchQueryRun::HeaderParseFailed { diagnostics, facts } => {
let block = diagnostics_block(diagnostics);
Ok(Response::ok(
ProofStateResult::HeaderParseFailed {
summary: block.summary,
diagnostics: block.diagnostics,
truncated: block.truncated,
query_facts: ctx.config.verbosity.is_full().then(|| project_query_facts(facts)),
},
freshness,
)
.with_runtime(run.runtime))
}
BatchQueryRun::Unsupported => {
Ok(Response::ok(ProofStateResult::Unsupported, freshness).with_runtime(run.runtime))
}
}
}
async fn proof_state_needs_build_response(
ctx: &ToolContext,
hint: ProjectHint,
imports: Vec<String>,
err: ServerError,
) -> Result<Response<ProofStateResult>> {
let base = ctx.broker.project_runtime(hint, imports).await?;
let message = err.to_string().lines().next().unwrap_or_default().trim().to_owned();
let response = Response::ok(needs_build_context(message), base.freshness).with_runtime(base.runtime);
Ok(crate::diagnosis::warn_needs_build(
response,
&crate::diagnosis::IncompleteCause::MissingOlean(err.to_string()),
))
}
fn needs_build_context(message: String) -> ProofStateResult {
ProofStateResult::Context {
diagnostics: DiagnosticsBlock {
summary: DiagnosticSummary::default(),
diagnostics: Vec::new(),
truncated: false,
},
declaration_name: None,
namespace_name: None,
goals_before: Vec::new(),
goals_after: Vec::new(),
locals: Vec::new(),
expected_type: None,
truncated: false,
total_truncated: false,
unavailable: Vec::new(),
needs_build: vec![SelectorMessage {
id: PROOF_STATE_CONTEXT_ID.to_owned(),
message,
}],
ambiguous: Vec::new(),
budget_exceeded: Vec::new(),
query_facts: None,
}
}
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceScope {
File,
Project,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct FindReferencesRequest {
pub name: String,
pub scope: ReferenceScope,
#[serde(default)]
pub file: Option<PathBuf>,
#[serde(default)]
pub files: Vec<PathBuf>,
#[serde(default)]
pub limit: Option<usize>,
#[serde(default)]
pub project: Option<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct HeaderParseFailedFile {
pub file: String,
pub diagnostics: ElabFailure,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct MissingImportsFile {
pub file: String,
pub missing: Vec<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum FindReferencesResult {
Ok {
references: Vec<ReferenceHit>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
truncated: bool,
files_scanned: usize,
files_skipped: usize,
#[serde(skip_serializing_if = "Vec::is_empty")]
unsupported_files: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
header_parse_failed_files: Vec<HeaderParseFailedFile>,
#[serde(skip_serializing_if = "Vec::is_empty")]
missing_imports_files: Vec<MissingImportsFile>,
semantic_based: bool,
},
InvalidRequest {
message: String,
semantic_based: bool,
},
}
pub async fn find_references(ctx: &ToolContext, req: FindReferencesRequest) -> Result<Response<FindReferencesResult>> {
let hint = ProjectHint::from_request(req.project.clone());
let meta = ctx.broker.resolve_meta(&hint)?;
let base = ctx.broker.project_runtime(hint.clone(), Vec::new()).await?;
let freshness = base.freshness;
let runtime = base.runtime;
let root = meta.canonical_root;
let limit = req.limit.unwrap_or(MAX_REFERENCES).min(MAX_REFERENCES);
match req.scope {
ReferenceScope::File => find_references_in_file(ctx, hint, &root, &req, freshness, runtime, limit).await,
ReferenceScope::Project => Ok(find_references_in_project(&root, &req, freshness, runtime, limit)),
}
}
async fn find_references_in_file(
ctx: &ToolContext,
hint: ProjectHint,
root: &Path,
req: &FindReferencesRequest,
freshness: Freshness,
mut runtime: RuntimeFacts,
limit: usize,
) -> Result<Response<FindReferencesResult>> {
let Some(file) = req.file.as_ref() else {
return Ok(Response::ok(
FindReferencesResult::InvalidRequest {
message: "find_references with scope=file requires `file`".to_owned(),
semantic_based: true,
},
freshness,
)
.with_runtime(runtime));
};
if !req.files.is_empty() {
return Ok(Response::ok(
FindReferencesResult::InvalidRequest {
message: "find_references with scope=file accepts `file`, not `files`".to_owned(),
semantic_based: true,
},
freshness,
)
.with_runtime(runtime));
}
let path = resolve_path(root, file);
let display = display_path(root, &path);
let query = LeanWorkerModuleQuery::References { name: req.name.clone() };
let mut hits: Vec<ReferenceHit> = Vec::new();
let mut unsupported_files: Vec<String> = Vec::new();
let mut header_parse_failed_files: Vec<HeaderParseFailedFile> = Vec::new();
let mut missing_imports_files: Vec<MissingImportsFile> = Vec::new();
let mut truncated = false;
let mut files_scanned = 0usize;
let mut files_skipped = 0usize;
let mut any_freshly_processed = false;
let mut needs_build: Option<ServerError> = None;
match run_module_query(ctx, hint, root, &path, query).await {
Ok(QueryRun {
outcome:
ModuleQueryRun::Ready {
result: LeanWorkerModuleQueryResult::References(result),
freshly_processed,
missing_imports,
},
runtime: run_runtime,
}) => {
runtime = run_runtime;
files_scanned = files_scanned.saturating_add(1);
any_freshly_processed |= freshly_processed;
if !missing_imports.is_empty() {
missing_imports_files.push(MissingImportsFile {
file: display.clone(),
missing: missing_imports,
});
}
if result.truncated {
truncated = true;
}
for node in &result.references {
if hits.len() >= limit {
truncated = true;
break;
}
hits.push(project_reference(&display, node));
}
}
Ok(QueryRun {
outcome:
ModuleQueryRun::Ready {
freshly_processed,
missing_imports,
..
},
runtime: run_runtime,
}) => {
runtime = run_runtime;
files_scanned = files_scanned.saturating_add(1);
any_freshly_processed |= freshly_processed;
if !missing_imports.is_empty() {
missing_imports_files.push(MissingImportsFile {
file: display,
missing: missing_imports,
});
}
}
Ok(QueryRun {
outcome: ModuleQueryRun::HeaderParseFailed { diagnostics },
runtime: run_runtime,
}) => {
runtime = run_runtime;
header_parse_failed_files.push(HeaderParseFailedFile {
file: display,
diagnostics,
});
}
Ok(QueryRun {
outcome: ModuleQueryRun::Unsupported,
runtime: run_runtime,
}) => {
runtime = run_runtime;
unsupported_files.push(display);
}
Err(ServerError::Io(_)) => {
files_skipped = files_skipped.saturating_add(1);
}
Err(err) if crate::diagnosis::missing_olean_failure(&err) => {
files_skipped = files_skipped.saturating_add(1);
needs_build = Some(err);
}
Err(err) => return Err(err),
}
hits.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.line.cmp(&b.line))
.then(a.column.cmp(&b.column))
});
let result = FindReferencesResult::Ok {
references: hits,
truncated,
files_scanned,
files_skipped,
unsupported_files,
header_parse_failed_files,
missing_imports_files,
semantic_based: true,
};
let response = attach_query_notes(
Response::ok(result, freshness).with_runtime(runtime),
any_freshly_processed,
&[],
false,
);
let response = match needs_build {
Some(err) => crate::diagnosis::warn_needs_build(
response,
&crate::diagnosis::IncompleteCause::MissingOlean(err.to_string()),
),
None => response,
};
Ok(response)
}
fn find_references_in_project(
root: &Path,
req: &FindReferencesRequest,
freshness: Freshness,
runtime: RuntimeFacts,
limit: usize,
) -> Response<FindReferencesResult> {
if req.file.is_some() {
return Response::ok(
FindReferencesResult::InvalidRequest {
message: "find_references with scope=project accepts `files`, not `file`".to_owned(),
semantic_based: true,
},
freshness,
)
.with_runtime(runtime);
}
let index = crate::ilean::references_to(root, &req.name);
if index.status == crate::ilean::IndexStatus::NotBuilt {
let response = Response::ok(empty_references_result(0, 0), freshness).with_runtime(runtime);
return crate::diagnosis::warn_needs_build(
response,
&crate::diagnosis::IncompleteCause::MissingImports(Vec::new()),
);
}
let restrict: Option<std::collections::HashSet<String>> = (!req.files.is_empty()).then(|| {
req.files
.iter()
.map(|p| display_path(root, &resolve_path(root, p)))
.collect()
});
let mut hits: Vec<ReferenceHit> = Vec::new();
for loc in &index.references {
let display = display_path(root, &loc.file);
if restrict.as_ref().is_some_and(|set| !set.contains(&display)) {
continue;
}
hits.push(index_reference(&display, loc));
}
hits.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.line.cmp(&b.line))
.then(a.column.cmp(&b.column))
});
let truncated = hits.len() > limit;
hits.truncate(limit);
let result = FindReferencesResult::Ok {
references: hits,
truncated,
files_scanned: index.modules_scanned,
files_skipped: index.modules_skipped,
unsupported_files: Vec::new(),
header_parse_failed_files: Vec::new(),
missing_imports_files: Vec::new(),
semantic_based: true,
};
let response = Response::ok(result, freshness).with_runtime(runtime);
if index.stale_sources.is_empty() {
return response;
}
let names = index
.stale_sources
.iter()
.take(3)
.map(|p| display_path(root, p))
.collect::<Vec<_>>()
.join(", ");
let more = index.stale_sources.len().saturating_sub(3);
let suffix = if more > 0 {
format!(", … (+{more} more)")
} else {
String::new()
};
response
.warn(format!(
"reference index is build-fresh, not edit-fresh: {} contributing module(s) have source newer than \
their .ilean ({names}{suffix}); results reflect the last `lake build`.",
index.stale_sources.len()
))
.hint("re-run `lake build` to refresh the reference index for edited modules")
}
fn empty_references_result(files_scanned: usize, files_skipped: usize) -> FindReferencesResult {
FindReferencesResult::Ok {
references: Vec::new(),
truncated: false,
files_scanned,
files_skipped,
unsupported_files: Vec::new(),
header_parse_failed_files: Vec::new(),
missing_imports_files: Vec::new(),
semantic_based: true,
}
}
fn index_reference(file: &str, loc: &crate::ilean::ReferenceLocation) -> ReferenceHit {
ReferenceHit {
file: file.to_owned(),
line: loc.start_line.saturating_add(1),
column: loc.start_column.saturating_add(1),
end_line: loc.end_line.saturating_add(1),
end_column: loc.end_column.saturating_add(1),
kind: match loc.kind {
crate::ilean::RefKind::Def => "def",
crate::ilean::RefKind::Ref => "ref",
},
}
}
struct QueryRun {
outcome: ModuleQueryRun,
runtime: RuntimeFacts,
}
enum ModuleQueryRun {
Ready {
result: LeanWorkerModuleQueryResult,
freshly_processed: bool,
missing_imports: Vec<String>,
},
HeaderParseFailed {
diagnostics: ElabFailure,
},
Unsupported,
}
struct BatchRun {
outcome: BatchQueryRun,
runtime: RuntimeFacts,
freshness: Freshness,
}
enum BatchQueryRun {
Ready {
result: lean_rs_worker_parent::LeanWorkerModuleQueryBatchEnvelope,
facts: LeanWorkerModuleQueryCacheFacts,
missing_imports: Vec<String>,
},
HeaderParseFailed {
diagnostics: ElabFailure,
facts: LeanWorkerModuleQueryCacheFacts,
},
Unsupported,
}
async fn run_module_query(
ctx: &ToolContext,
hint: ProjectHint,
root: &Path,
path: &Path,
query: LeanWorkerModuleQuery,
) -> Result<QueryRun> {
let input = read_query_file(root, path)?;
let session_imports = session_imports(header_imports(&input.source));
let call = ctx
.broker
.process_cached_module_query(
hint,
input.resolved,
input.hash,
session_imports,
input.imports,
input.source,
query,
LeanWorkerElabOptions::new(),
)
.await?;
Ok(QueryRun {
outcome: route_query_outcome(call.value, call.freshly_processed),
runtime: call.runtime,
})
}
async fn run_module_query_batch(
ctx: &ToolContext,
hint: ProjectHint,
root: &Path,
path: &Path,
selectors: Vec<LeanWorkerModuleQuerySelector>,
budgets: LeanWorkerOutputBudgets,
) -> Result<BatchRun> {
let input = read_query_file(root, path)?;
let file_label = input.resolved.to_string_lossy().into_owned();
let session_imports = session_imports(header_imports(&input.source));
let call = ctx
.broker
.process_cached_module_query_batch(
hint,
input.resolved,
input.hash,
session_imports,
input.imports.clone(),
input.source,
selectors,
budgets,
LeanWorkerElabOptions::new().file_label(&file_label),
)
.await?;
Ok(BatchRun {
outcome: route_batch_outcome(call.value),
runtime: call.runtime,
freshness: call.freshness,
})
}
fn proof_agent_budgets() -> LeanWorkerOutputBudgets {
LeanWorkerOutputBudgets {
per_field_bytes: PROOF_AGENT_PER_FIELD_BYTES,
total_bytes: DEFAULT_TOTAL_BYTES,
}
}
fn expert_query_budgets() -> LeanWorkerOutputBudgets {
LeanWorkerOutputBudgets {
per_field_bytes: EXPERT_QUERY_PER_FIELD_BYTES,
total_bytes: DEFAULT_TOTAL_BYTES,
}
}
#[allow(
dead_code,
reason = "prompt 38 establishes the shared budget helper for reference-query callers"
)]
fn reference_query_budgets() -> LeanWorkerOutputBudgets {
expert_query_budgets()
}
fn route_query_outcome(outcome: LeanWorkerModuleQueryOutcome, freshly_processed: bool) -> ModuleQueryRun {
match outcome {
LeanWorkerModuleQueryOutcome::Ok { result, .. } => ModuleQueryRun::Ready {
result,
freshly_processed,
missing_imports: Vec::new(),
},
LeanWorkerModuleQueryOutcome::MissingImports { result, missing, .. } => ModuleQueryRun::Ready {
result,
freshly_processed,
missing_imports: missing,
},
LeanWorkerModuleQueryOutcome::HeaderParseFailed { diagnostics } => ModuleQueryRun::HeaderParseFailed {
diagnostics: project_failure(&diagnostics),
},
LeanWorkerModuleQueryOutcome::Unsupported => ModuleQueryRun::Unsupported,
_ => ModuleQueryRun::Unsupported,
}
}
fn route_batch_outcome(outcome: LeanWorkerModuleQueryBatchOutcome) -> BatchQueryRun {
match outcome {
LeanWorkerModuleQueryBatchOutcome::Ok { result, facts, .. } => BatchQueryRun::Ready {
result,
facts,
missing_imports: Vec::new(),
},
LeanWorkerModuleQueryBatchOutcome::MissingImports {
result, missing, facts, ..
} => BatchQueryRun::Ready {
result,
facts,
missing_imports: missing,
},
LeanWorkerModuleQueryBatchOutcome::HeaderParseFailed { diagnostics, facts } => {
BatchQueryRun::HeaderParseFailed {
diagnostics: project_failure(&diagnostics),
facts,
}
}
LeanWorkerModuleQueryBatchOutcome::Unsupported => BatchQueryRun::Unsupported,
_ => BatchQueryRun::Unsupported,
}
}
enum ProjectedBatchItem {
Ok { id: String, result: BatchProjection },
Unavailable { id: String, message: String },
BudgetExceeded { id: String, message: String },
}
fn project_batch_item(item: LeanWorkerModuleQueryBatchItem, file: Option<&str>) -> ProjectedBatchItem {
match item {
LeanWorkerModuleQueryBatchItem::Ok { id, result } => ProjectedBatchItem::Ok {
id,
result: project_batch_result(*result, file),
},
LeanWorkerModuleQueryBatchItem::Unavailable { id, message } => ProjectedBatchItem::Unavailable { id, message },
LeanWorkerModuleQueryBatchItem::BudgetExceeded { id, message } => {
ProjectedBatchItem::BudgetExceeded { id, message }
}
_ => ProjectedBatchItem::Unavailable {
id: "<unknown>".to_owned(),
message: "worker returned an unknown selector item".to_owned(),
},
}
}
fn project_batch_result(result: LeanWorkerModuleQueryBatchResult, file: Option<&str>) -> BatchProjection {
match result {
LeanWorkerModuleQueryBatchResult::Diagnostics(failure) => {
BatchProjection::Diagnostics(diagnostics_block(project_failure(&failure)))
}
LeanWorkerModuleQueryBatchResult::ProofState(result) => {
BatchProjection::ProofState(project_proof_state_result(result))
}
LeanWorkerModuleQueryBatchResult::TypeAt(result) => BatchProjection::TypeAt(project_type_at_result(result)),
LeanWorkerModuleQueryBatchResult::References(result) => {
let display = file.unwrap_or("");
BatchProjection::References(ReferencesProjection {
references: result
.references
.iter()
.map(|node| project_reference(display, node))
.collect(),
truncated: result.truncated,
})
}
LeanWorkerModuleQueryBatchResult::DeclarationTarget(result) => {
BatchProjection::DeclarationTarget(project_declaration_target_result(result))
}
LeanWorkerModuleQueryBatchResult::SurroundingDeclaration(result) => {
BatchProjection::SurroundingDeclaration(project_surrounding_declaration_result(result))
}
_ => BatchProjection::Diagnostics(DiagnosticsBlock {
summary: DiagnosticSummary::default(),
diagnostics: Vec::new(),
truncated: false,
}),
}
}
fn project_proof_state_result(result: LeanWorkerProofStateResult) -> ProofStateProjection {
match result {
LeanWorkerProofStateResult::State { info } => ProofStateProjection::State {
info: Box::new(project_proof_state_info(*info)),
},
LeanWorkerProofStateResult::Unavailable { message } => ProofStateProjection::Unavailable { message },
LeanWorkerProofStateResult::Ambiguous { candidates } => ProofStateProjection::Ambiguous {
candidates: candidates.into_iter().map(project_declaration_target_info).collect(),
},
LeanWorkerProofStateResult::NeedsBuild { missing } => ProofStateProjection::NeedsBuild { missing },
_ => ProofStateProjection::Unavailable {
message: "worker returned an unknown proof-state result".to_owned(),
},
}
}
fn project_proof_state_info(info: LeanWorkerProofStateInfo) -> ProofStateContext {
ProofStateContext {
declaration_name: info.declaration_name,
namespace_name: info.namespace_name,
goals_before: info.goals_before,
goals_after: info.goals_after,
locals: info.locals.into_iter().map(project_local_info).collect(),
expected_type: info.expected_type.map(rendered_text),
truncated: info.truncated,
}
}
fn project_local_info(info: LeanWorkerLocalInfo) -> LocalInfo {
LocalInfo {
name: info.name,
binder_info: info.binder_info,
type_str: rendered_text(info.type_str),
value: info.value.map(rendered_text),
}
}
fn project_type_at_result(result: LeanWorkerTypeAtResult) -> TypeAtProjection {
match result {
LeanWorkerTypeAtResult::Term {
span,
expr,
type_str,
expected_type,
} => TypeAtProjection::Term {
expr: rendered_text(expr),
type_str: rendered_text(type_str),
expected_type: expected_type.map(rendered_text),
span: span_of_module(span),
},
LeanWorkerTypeAtResult::NoTerm => TypeAtProjection::NoTerm,
_ => TypeAtProjection::NoTerm,
}
}
fn project_declaration_target_result(result: LeanWorkerDeclarationTargetResult) -> DeclarationTargetProjection {
match result {
LeanWorkerDeclarationTargetResult::Target { info } => DeclarationTargetProjection::Target {
info: project_declaration_target_info(info),
},
LeanWorkerDeclarationTargetResult::NotFound => DeclarationTargetProjection::NotFound,
LeanWorkerDeclarationTargetResult::Ambiguous { candidates } => DeclarationTargetProjection::Ambiguous {
candidates: candidates.into_iter().map(project_declaration_target_info).collect(),
},
_ => DeclarationTargetProjection::NotFound,
}
}
fn project_surrounding_declaration_result(
result: LeanWorkerSurroundingDeclarationResult,
) -> SurroundingDeclarationProjection {
match result {
LeanWorkerSurroundingDeclarationResult::Declaration { info } => SurroundingDeclarationProjection::Declaration {
info: project_declaration_target_info(info),
},
LeanWorkerSurroundingDeclarationResult::None => SurroundingDeclarationProjection::None,
_ => SurroundingDeclarationProjection::None,
}
}
fn competing_decls(candidates: &[DeclarationTargetInfo]) -> Vec<crate::diagnosis::CompetingDecl> {
candidates
.iter()
.map(|info| crate::diagnosis::CompetingDecl {
name: info.declaration_name.clone(),
namespace: (!info.namespace_name.is_empty()).then(|| info.namespace_name.clone()),
})
.collect()
}
fn project_declaration_target_info(info: LeanWorkerDeclarationTargetInfo) -> DeclarationTargetInfo {
DeclarationTargetInfo {
short_name: info.short_name,
declaration_name: info.declaration_name,
namespace_name: info.namespace_name,
declaration_kind: info.declaration_kind,
declaration_span: span_of_module(info.declaration_span),
name_span: span_of_module(info.name_span),
body_span: span_of_module(info.body_span),
}
}
fn diagnostics_block(failure: ElabFailure) -> DiagnosticsBlock {
let ElabFailure { diagnostics, truncated } = failure;
let diagnostics = sort_diagnostics(diagnostics);
DiagnosticsBlock {
summary: DiagnosticSummary::from_diagnostics(&diagnostics),
diagnostics,
truncated,
}
}
fn sort_diagnostics(mut diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
diagnostics.sort_by_key(|d| d.position.as_ref().map_or((u32::MAX, u32::MAX), |p| (p.line, p.column)));
diagnostics
}
fn display_path(root: &Path, path: &Path) -> String {
path.strip_prefix(root).unwrap_or(path).to_string_lossy().into_owned()
}
fn span_of_module(span: LeanWorkerModuleSourceSpan) -> SourceSpan {
SourceSpan {
start_line: span.start_line,
start_column: span.start_column,
end_line: span.end_line,
end_column: span.end_column,
}
}
fn rendered_text(info: LeanWorkerRenderedInfo) -> RenderedText {
RenderedText {
value: info.value,
truncated: info.truncated,
}
}
fn project_query_facts(facts: LeanWorkerModuleQueryCacheFacts) -> ModuleQueryFacts {
ModuleQueryFacts {
cache_status: cache_status_label(facts.cache_status),
output_bytes: facts.output_bytes,
cache_entry_count: facts.cache_entry_count,
cache_approx_bytes: facts.cache_approx_bytes,
timings: project_query_timings(facts.timings),
}
}
fn cache_status_label(status: LeanWorkerModuleCacheStatus) -> &'static str {
match status {
LeanWorkerModuleCacheStatus::Hit => "hit",
LeanWorkerModuleCacheStatus::Miss => "miss",
LeanWorkerModuleCacheStatus::Rebuilt => "rebuilt",
LeanWorkerModuleCacheStatus::Evicted => "evicted",
_ => "unknown",
}
}
fn project_query_timings(timings: LeanWorkerModuleQueryTimings) -> ModuleQueryTimings {
ModuleQueryTimings {
header_import_micros: timings.header_import_micros,
elaboration_micros: timings.elaboration_micros,
projection_micros: timings.projection_micros,
rendering_micros: timings.rendering_micros,
}
}
fn project_reference(file: &str, node: &LeanWorkerNameRef) -> ReferenceHit {
ReferenceHit {
file: file.to_owned(),
line: node.start_line,
column: node.start_column,
end_line: node.end_line,
end_column: node.end_column,
kind: if node.is_binder { "def" } else { "ref" },
}
}
fn attach_query_notes<T>(
mut response: Response<T>,
freshly_processed: bool,
missing_imports: &[String],
batch: bool,
) -> Response<T>
where
T: Serialize + JsonSchema,
{
if freshly_processed {
let kind = if batch { "module query batch" } else { "module query" };
response.next_actions.push(format!(
"{kind} result cached; repeating the same query against the same file contents reuses it"
));
}
warn_session_missing_imports(response, missing_imports)
}
fn warn_session_missing_imports<T>(mut response: Response<T>, missing_imports: &[String]) -> Response<T>
where
T: Serialize + JsonSchema,
{
if !missing_imports.is_empty() {
response.warnings.push(format!(
"file header referenced imports not present in the opened session: {}",
missing_imports.join(", ")
));
}
response
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
clippy::indexing_slicing,
reason = "unit tests should fail directly when literal JSON fixtures stop matching the schema"
)]
mod tests {
use lean_rs_worker_parent::{
LeanWorkerElabFailure, LeanWorkerModuleCacheStatus, LeanWorkerModuleQueryBatchEnvelope,
LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryCacheFacts, LeanWorkerModuleQueryTimings,
};
use super::{
BatchQueryRun, DiagnosticSummary, DiagnosticsBlock, LeanWorkerProofPositionSelector, ProofPositionSelector,
ProofStateRequest, ProofStateResult, RenderedText, cache_status_label, expert_query_budgets,
needs_build_context, project_query_facts, proof_agent_budgets, reference_query_budgets, route_batch_outcome,
worker_proof_position,
};
use crate::tools::source_input::{header_imports, read_query_file};
fn worker_facts(status: LeanWorkerModuleCacheStatus) -> LeanWorkerModuleQueryCacheFacts {
LeanWorkerModuleQueryCacheFacts {
cache_status: status,
timings: LeanWorkerModuleQueryTimings {
header_import_micros: 1,
elaboration_micros: 2,
projection_micros: 3,
rendering_micros: 4,
},
output_bytes: 123,
cache_entry_count: Some(2),
cache_approx_bytes: Some(4096),
resource: None,
}
}
#[test]
fn header_imports_handles_public_meta_and_all() {
let source = "\
module
public import Foo.Bar
meta import Baz.Qux
import all Project.Internal
import Init -- comment
";
assert_eq!(
header_imports(source),
vec![
"Foo.Bar".to_owned(),
"Baz.Qux".to_owned(),
"Project.Internal".to_owned(),
"Init".to_owned(),
]
);
}
#[test]
fn proof_state_request_round_trips() {
let request: ProofStateRequest =
serde_json::from_str(r#"{"file":"Foo/Bar.lean","declaration":"Foo.Bar.t"}"#).unwrap();
assert_eq!(request.declaration, "Foo.Bar.t");
assert!(matches!(request.proof_position, ProofPositionSelector::Default));
}
#[test]
fn default_position_maps_to_pristine_entry() {
assert!(matches!(
worker_proof_position(ProofPositionSelector::Default),
LeanWorkerProofPositionSelector::Entry
));
assert!(matches!(
worker_proof_position(ProofPositionSelector::Index { index: 0 }),
LeanWorkerProofPositionSelector::Index { index: 0 }
));
}
#[test]
fn module_query_budget_helpers_set_host_policy() {
let proof_agent = proof_agent_budgets();
assert_eq!(proof_agent.per_field_bytes, 4 * 1024);
assert_eq!(proof_agent.total_bytes, 64 * 1024);
let expert = expert_query_budgets();
assert_eq!(expert.per_field_bytes, 8 * 1024);
assert_eq!(expert.total_bytes, 64 * 1024);
let references = reference_query_budgets();
assert_eq!(references, expert);
}
#[test]
fn needs_build_context_reports_a_needs_build_selector_not_unavailable() {
let result = needs_build_context("object file 'Dep.olean' ... does not exist".to_owned());
let value = serde_json::to_value(&result).unwrap();
assert_eq!(value["status"], "context");
assert_eq!(value["needs_build"][0]["id"], "proof_state");
assert!(value["needs_build"][0]["message"].as_str().unwrap().contains(".olean"));
assert!(value.get("unavailable").is_none());
assert!(value.get("goals_before").is_none());
}
#[test]
fn proof_state_context_serializes_flattened_workflow_fields() {
let result = ProofStateResult::Context {
diagnostics: DiagnosticsBlock {
summary: DiagnosticSummary::default(),
diagnostics: Vec::new(),
truncated: false,
},
declaration_name: Some("Demo.proof".to_owned()),
namespace_name: Some("Demo".to_owned()),
goals_before: vec!["⊢ True".to_owned()],
goals_after: Vec::new(),
locals: Vec::new(),
expected_type: Some(RenderedText {
value: "True".to_owned(),
truncated: false,
}),
truncated: false,
total_truncated: false,
unavailable: Vec::new(),
needs_build: Vec::new(),
ambiguous: Vec::new(),
budget_exceeded: Vec::new(),
query_facts: Some(Box::new(project_query_facts(worker_facts(
LeanWorkerModuleCacheStatus::Miss,
)))),
};
let value = serde_json::to_value(&result).unwrap();
assert_eq!(value["status"], "context");
assert_eq!(value["declaration_name"], "Demo.proof");
assert_eq!(value["namespace_name"], "Demo");
assert_eq!(value["goals_before"][0], "⊢ True");
assert_eq!(value["expected_type"]["value"], "True");
assert!(value.get("span").is_none());
assert!(value.get("safe_edit").is_none());
assert!(value.get("proof_state").is_none());
assert!(value.get("declaration_target").is_none());
}
#[test]
fn query_facts_project_cache_status_and_timings() {
let facts = project_query_facts(worker_facts(LeanWorkerModuleCacheStatus::Hit));
assert_eq!(facts.cache_status, "hit");
assert_eq!(facts.output_bytes, 123);
assert_eq!(facts.cache_entry_count, Some(2));
assert_eq!(facts.cache_approx_bytes, Some(4096));
assert_eq!(facts.timings.header_import_micros, 1);
assert_eq!(facts.timings.elaboration_micros, 2);
assert_eq!(facts.timings.projection_micros, 3);
assert_eq!(facts.timings.rendering_micros, 4);
}
#[test]
fn cache_status_labels_match_wire_strings() {
assert_eq!(cache_status_label(LeanWorkerModuleCacheStatus::Hit), "hit");
assert_eq!(cache_status_label(LeanWorkerModuleCacheStatus::Miss), "miss");
assert_eq!(cache_status_label(LeanWorkerModuleCacheStatus::Rebuilt), "rebuilt");
assert_eq!(cache_status_label(LeanWorkerModuleCacheStatus::Evicted), "evicted");
}
#[test]
fn read_query_file_uses_canonical_path_for_identity() {
let dir = tempfile::tempdir().unwrap();
let module_dir = dir.path().join("Demo");
std::fs::create_dir(&module_dir).unwrap();
std::fs::write(module_dir.join("Basic.lean"), "import Init\n#check Nat\n").unwrap();
let input = read_query_file(dir.path(), std::path::Path::new("Demo/../Demo/Basic.lean")).unwrap();
assert_eq!(input.resolved, module_dir.join("Basic.lean").canonicalize().unwrap());
assert_eq!(input.imports, vec!["Init".to_owned()]);
assert_eq!(input.source, "import Init\n#check Nat\n");
}
#[test]
fn route_batch_outcome_preserves_worker_facts() {
let result = LeanWorkerModuleQueryBatchEnvelope {
items: Vec::new(),
total_truncated: false,
};
let outcome = route_batch_outcome(LeanWorkerModuleQueryBatchOutcome::Ok {
result,
imports: Vec::new(),
facts: worker_facts(LeanWorkerModuleCacheStatus::Hit),
});
let BatchQueryRun::Ready { facts, .. } = outcome else {
panic!("expected ready batch outcome");
};
assert_eq!(cache_status_label(facts.cache_status), "hit");
let outcome = route_batch_outcome(LeanWorkerModuleQueryBatchOutcome::MissingImports {
result: LeanWorkerModuleQueryBatchEnvelope {
items: Vec::new(),
total_truncated: false,
},
imports: Vec::new(),
missing: vec!["Missing.Mod".to_owned()],
facts: worker_facts(LeanWorkerModuleCacheStatus::Rebuilt),
});
let BatchQueryRun::Ready {
facts, missing_imports, ..
} = outcome
else {
panic!("expected ready batch outcome with missing imports");
};
assert_eq!(cache_status_label(facts.cache_status), "rebuilt");
assert_eq!(missing_imports, vec!["Missing.Mod".to_owned()]);
let outcome = route_batch_outcome(LeanWorkerModuleQueryBatchOutcome::HeaderParseFailed {
diagnostics: LeanWorkerElabFailure {
diagnostics: Vec::new(),
truncated: false,
},
facts: worker_facts(LeanWorkerModuleCacheStatus::Evicted),
});
let BatchQueryRun::HeaderParseFailed { facts, .. } = outcome else {
panic!("expected header-parse failure");
};
assert_eq!(cache_status_label(facts.cache_status), "evicted");
}
}