1mod javascript;
9mod lean;
10mod rocq;
11mod rust_lang;
12mod tree;
13
14use std::collections::BTreeMap;
15use std::rc::Rc;
16
17use super::analysis::syntax_facts;
18use super::{ProgramBinding, ProgramDiagnostic, ProgramFact, ProgramRange};
19use crate::{LinkNetwork, ParseConfiguration};
20use tree::{SyntaxTree, Target};
21
22const TYPE_KINDS: [&str; 9] = [
23 "class",
24 "struct",
25 "enum",
26 "trait",
27 "type",
28 "structure",
29 "inductive",
30 "constructor",
31 "universe",
32];
33const EFFECT_TRAITS: [&str; 4] = ["async", "generator", "io", "proof-state"];
34const PROOF_KINDS: [&str; 3] = ["theorem", "lemma", "tactic"];
35const PROOF_ROLES: [&str; 3] = ["tactic", "assertion", "const-assertion"];
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ProgramProjectSource {
41 path: String,
42 source: String,
43}
44
45impl ProgramProjectSource {
46 #[must_use]
48 pub fn new(path: impl Into<String>, source: impl Into<String>) -> Self {
49 Self {
50 path: path.into(),
51 source: source.into(),
52 }
53 }
54
55 #[must_use]
57 pub fn path(&self) -> &str {
58 &self.path
59 }
60
61 #[must_use]
63 pub fn source(&self) -> &str {
64 &self.source
65 }
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
70pub struct ProgramProjectContext {
71 root: String,
72 files: Vec<String>,
73 dependencies: Vec<String>,
74 extensions: Vec<String>,
75 entry: Option<String>,
76 sources: Vec<ProgramProjectSource>,
77}
78
79impl ProgramProjectContext {
80 #[must_use]
82 pub fn new(root: impl Into<String>, files: Vec<String>, dependencies: Vec<String>) -> Self {
83 Self {
84 root: root.into(),
85 files,
86 dependencies,
87 extensions: Vec::new(),
88 entry: None,
89 sources: Vec::new(),
90 }
91 }
92
93 #[must_use]
95 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
96 self.extensions = extensions;
97 self
98 }
99
100 #[must_use]
103 pub fn with_entry(
104 mut self,
105 entry: impl Into<String>,
106 sources: Vec<ProgramProjectSource>,
107 ) -> Self {
108 self.entry = Some(entry.into());
109 self.sources = sources;
110 self
111 }
112
113 #[must_use]
115 pub fn root(&self) -> &str {
116 &self.root
117 }
118
119 #[must_use]
121 pub fn files(&self) -> &[String] {
122 &self.files
123 }
124
125 #[must_use]
127 pub fn dependencies(&self) -> &[String] {
128 &self.dependencies
129 }
130
131 #[must_use]
133 pub fn extensions(&self) -> &[String] {
134 &self.extensions
135 }
136
137 #[must_use]
139 pub fn entry(&self) -> Option<&str> {
140 self.entry.as_deref()
141 }
142
143 #[must_use]
145 pub fn sources(&self) -> &[ProgramProjectSource] {
146 &self.sources
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct ProgramProjectModule {
153 request: String,
154 module: String,
155 file: String,
156 range: ProgramRange,
157}
158
159impl ProgramProjectModule {
160 #[must_use]
162 pub fn request(&self) -> &str {
163 &self.request
164 }
165
166 #[must_use]
168 pub fn module(&self) -> &str {
169 &self.module
170 }
171
172 #[must_use]
174 pub fn file(&self) -> &str {
175 &self.file
176 }
177
178 #[must_use]
180 pub const fn range(&self) -> ProgramRange {
181 self.range
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct ProgramProjectReference {
189 role: &'static str,
190 name: String,
191 range: ProgramRange,
192 symbol: String,
193 target_kind: String,
194 traits: Vec<String>,
195 file: String,
196 declaration: ProgramRange,
197}
198
199impl ProgramProjectReference {
200 #[must_use]
204 pub const fn role(&self) -> &'static str {
205 self.role
206 }
207
208 #[must_use]
210 pub fn name(&self) -> &str {
211 &self.name
212 }
213
214 #[must_use]
216 pub const fn range(&self) -> ProgramRange {
217 self.range
218 }
219
220 #[must_use]
222 pub fn symbol(&self) -> &str {
223 &self.symbol
224 }
225
226 #[must_use]
228 pub fn target_kind(&self) -> &str {
229 &self.target_kind
230 }
231
232 #[must_use]
234 pub fn traits(&self) -> &[String] {
235 &self.traits
236 }
237
238 #[must_use]
240 pub fn file(&self) -> &str {
241 &self.file
242 }
243
244 #[must_use]
246 pub const fn declaration(&self) -> ProgramRange {
247 self.declaration
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct ProgramExpansion {
254 name: String,
255 kind: &'static str,
256 range: ProgramRange,
257 expansion: String,
258 target: String,
259}
260
261impl ProgramExpansion {
262 #[must_use]
264 pub fn name(&self) -> &str {
265 &self.name
266 }
267
268 #[must_use]
270 pub const fn kind(&self) -> &'static str {
271 self.kind
272 }
273
274 #[must_use]
276 pub const fn range(&self) -> ProgramRange {
277 self.range
278 }
279
280 #[must_use]
282 pub fn expansion(&self) -> &str {
283 &self.expansion
284 }
285
286 #[must_use]
288 pub fn target(&self) -> &str {
289 &self.target
290 }
291}
292
293#[derive(Clone, Debug, Default)]
295pub(super) struct ProjectResult {
296 pub(super) modules: Vec<ProgramProjectModule>,
297 pub(super) facts: Vec<ProgramFact>,
298 pub(super) references: Vec<ProgramProjectReference>,
299 pub(super) expansions: Vec<ProgramExpansion>,
300 pub(super) diagnostics: Vec<ProgramDiagnostic>,
301 pub(super) requests: Vec<ProgramFact>,
303 pub(super) resolved_requests: Vec<(String, usize)>,
305}
306
307pub(super) fn project_semantics(
310 language: &'static str,
311 source: &str,
312 mappings: &[super::ProgramSourceMapping],
313 bindings: &[ProgramBinding],
314 unresolved: &[ProgramFact],
315 project: &ProgramProjectContext,
316) -> ProjectResult {
317 let Some(entry) = project.entry().filter(|entry| !entry.is_empty()) else {
318 return ProjectResult::default();
319 };
320 let mut context = ProjectContext::new(language, entry, source, mappings, project);
321 context.bindings = bindings;
322 context.unresolved = unresolved;
323 match language {
324 "JavaScript" => javascript::analyze(&mut context),
325 "Rust" => rust_lang::analyze(&mut context),
326 "Lean" => lean::analyze(&mut context),
327 "Rocq" => rocq::analyze(&mut context),
328 _ => {}
329 }
330 for file in &project.sources {
331 if context
332 .parsed
333 .get(&file.path)
334 .is_some_and(|parsed| !parsed.clean)
335 {
336 context.result.diagnostics.push(ProgramDiagnostic {
337 kind: "project-parse-error",
338 term: file.path.clone(),
339 range: ProgramRange::default(),
340 });
341 }
342 }
343 context.result
344}
345
346#[derive(Debug)]
348struct ParsedFile {
349 path: String,
350 source: String,
351 tree: SyntaxTree,
352 clean: bool,
353}
354
355struct ProjectContext<'a> {
356 language: &'static str,
357 bindings: &'a [ProgramBinding],
358 unresolved: &'a [ProgramFact],
359 entry: Rc<ParsedFile>,
360 sources: BTreeMap<&'a str, &'a str>,
361 parsed: BTreeMap<String, Rc<ParsedFile>>,
362 result: ProjectResult,
363}
364
365impl<'a> ProjectContext<'a> {
366 fn new(
367 language: &'static str,
368 entry: &str,
369 source: &str,
370 mappings: &[super::ProgramSourceMapping],
371 project: &'a ProgramProjectContext,
372 ) -> Self {
373 let mut context = Self {
374 language,
375 bindings: &[],
376 unresolved: &[],
377 entry: Rc::new(ParsedFile {
378 path: entry.to_string(),
379 source: source.to_string(),
380 tree: SyntaxTree::build(mappings, source),
381 clean: true,
382 }),
383 sources: project
384 .sources
385 .iter()
386 .map(|file| (file.path.as_str(), file.source.as_str()))
387 .collect(),
388 parsed: BTreeMap::new(),
389 result: ProjectResult::default(),
390 };
391 for file in &project.sources {
392 context.fact(
393 "project-file",
394 &file.path,
395 &file.path,
396 (0, file.source.len()),
397 );
398 }
399 context
400 }
401
402 fn has(&self, path: &str) -> bool {
403 path == self.entry.path || self.sources.contains_key(path)
404 }
405
406 fn text(&self, path: &str) -> Option<&str> {
407 if path == self.entry.path {
408 return Some(&self.entry.source);
409 }
410 self.sources.get(path).copied()
411 }
412
413 fn load(&mut self, path: &str) -> Option<Rc<ParsedFile>> {
415 self.load_as(path, self.language)
416 }
417
418 fn load_as(&mut self, path: &str, language: &str) -> Option<Rc<ParsedFile>> {
419 if path == self.entry.path {
420 return Some(Rc::clone(&self.entry));
421 }
422 let source = *self.sources.get(path)?;
423 let parsed = self.parsed.entry(path.to_string()).or_insert_with(|| {
424 let network = LinkNetwork::parse(source, language, ParseConfiguration::default());
425 Rc::new(ParsedFile {
426 path: path.to_string(),
427 source: source.to_string(),
428 tree: SyntaxTree::build(&syntax_facts(&network), source),
429 clean: network.verify_full_match(None).is_clean(),
430 })
431 });
432 Some(Rc::clone(parsed))
433 }
434
435 fn fact(&mut self, kind: &str, name: &str, file: &str, range: (usize, usize)) {
436 let mut fact = ProgramFact::new(kind, name, ProgramRange::new(range.0, range.1));
437 fact.file = Some(file.to_string());
438 self.result.facts.push(fact);
439 }
440
441 fn module(&mut self, request: &str, module: &str, file: &str, range: (usize, usize)) {
442 self.result.modules.push(ProgramProjectModule {
443 request: request.to_string(),
444 module: module.to_string(),
445 file: file.to_string(),
446 range: ProgramRange::new(range.0, range.1),
447 });
448 self.result
449 .resolved_requests
450 .push((request.to_string(), range.0));
451 }
452
453 fn request(&mut self, name: &str, range: (usize, usize)) {
455 self.result.requests.push(ProgramFact::new(
456 "module-import",
457 name,
458 ProgramRange::new(range.0, range.1),
459 ));
460 }
461
462 fn reference(
464 &mut self,
465 role: &'static str,
466 name: &str,
467 range: (usize, usize),
468 target: &Target,
469 ) {
470 self.result.references.push(ProgramProjectReference {
471 role,
472 name: name.to_string(),
473 range: ProgramRange::new(range.0, range.1),
474 symbol: target.symbol.clone(),
475 target_kind: target.kind.clone(),
476 traits: target.traits.clone(),
477 file: target.file.clone(),
478 declaration: ProgramRange::new(target.start, target.end),
479 });
480 }
481
482 fn expansion(
483 &mut self,
484 name: &str,
485 kind: &'static str,
486 range: (usize, usize),
487 expansion: String,
488 target: &str,
489 ) {
490 self.result.expansions.push(ProgramExpansion {
491 name: name.to_string(),
492 kind,
493 range: ProgramRange::new(range.0, range.1),
494 expansion,
495 target: target.to_string(),
496 });
497 }
498
499 fn diagnose(&mut self, kind: &'static str, term: String, range: (usize, usize)) {
500 self.result.diagnostics.push(ProgramDiagnostic {
501 kind,
502 term,
503 range: ProgramRange::new(range.0, range.1),
504 });
505 }
506}
507
508pub(super) fn project_evidence(
510 project: &ProgramProjectContext,
511 result: &ProjectResult,
512 construct: &str,
513) -> Vec<ProgramFact> {
514 let entry = project.entry().unwrap_or_default();
515 let located = |kind: String, name: &str, range: ProgramRange, file: &str| {
516 let mut fact = ProgramFact::new(kind, name, range);
517 fact.file = Some(file.to_string());
518 fact
519 };
520 let references = |predicate: &dyn Fn(&ProgramProjectReference) -> bool| {
521 result
522 .references
523 .iter()
524 .filter(|reference| predicate(reference))
525 .map(|reference| {
526 located(
527 format!(
528 "project-reference:{}:{}",
529 reference.role, reference.target_kind
530 ),
531 &reference.symbol,
532 reference.range,
533 entry,
534 )
535 })
536 .collect::<Vec<_>>()
537 };
538 let has_trait = |reference: &ProgramProjectReference, traits: &[&str]| {
539 reference
540 .traits
541 .iter()
542 .any(|value| traits.contains(&value.as_str()))
543 };
544 match construct {
545 "modules-and-imports" => result
546 .modules
547 .iter()
548 .map(|module| {
549 located(
550 "project-module".to_string(),
551 &module.module,
552 module.range,
553 entry,
554 )
555 })
556 .chain(references(&|reference| reference.role == "import"))
557 .collect(),
558 "scopes-and-bindings" => references(&|reference| reference.role == "reference"),
559 "recursive-definitions" => references(&|reference| has_trait(reference, &["recursive"])),
560 "types-and-universes" => references(&|reference| {
561 TYPE_KINDS.contains(&reference.target_kind.as_str())
562 || has_trait(reference, &["universe-polymorphic"])
563 }),
564 "effects" => references(&|reference| has_trait(reference, &EFFECT_TRAITS)),
565 "attributes" => references(&|reference| reference.role == "attribute"),
566 "macros-and-notation" => {
567 references(&|reference| matches!(reference.role, "macro" | "notation" | "template-tag"))
568 }
569 "proof-terms-and-tactics" => references(&|reference| {
570 PROOF_ROLES.contains(&reference.role)
571 || PROOF_KINDS.contains(&reference.target_kind.as_str())
572 }),
573 "surface-expansion-elaboration-traces" => result
574 .expansions
575 .iter()
576 .map(|expansion| {
577 located(
578 format!("expansion:{}", expansion.kind),
579 &expansion.target,
580 expansion.range,
581 entry,
582 )
583 })
584 .collect(),
585 "project-context-and-dependencies" => result.facts.clone(),
586 _ => Vec::new(),
587 }
588}