Skip to main content

meta_language/
program_representation.rs

1//! Editable program representations for JavaScript, Rust, Lean, and Rocq.
2
3mod analysis;
4mod edit;
5mod module_resolution;
6mod project;
7mod snapshot;
8
9pub use project::{
10    ProgramExpansion, ProgramProjectContext, ProgramProjectModule, ProgramProjectReference,
11    ProgramProjectSource,
12};
13pub use snapshot::{PROGRAM_SNAPSHOT_SCHEMA_VERSION, construct_program_from_fragments};
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::error::Error;
17use std::fmt;
18
19use crate::decorators::{DecoratorLevel, DecoratorSet, decorator_record, record_field};
20use crate::{LinkNetwork, LinkType, ParseConfiguration, language_support};
21use analysis::{
22    SemanticToken, effect_markers, extension_markers, module_markers, proof_markers,
23    resolve_bindings, scope_by_id, semantic_tokens, syntax_facts, unique_facts,
24    validate_identifier,
25};
26use module_resolution::{module_requests, project_has_module};
27
28/// Schema revision for four-language program representations.
29pub const PROGRAM_REPRESENTATION_SCHEMA_VERSION: u32 = 1;
30
31/// Semantic construct categories audited for every four-language frontend.
32pub const SEMANTIC_CONSTRUCTS: [&str; 10] = [
33    "modules-and-imports",
34    "scopes-and-bindings",
35    "recursive-definitions",
36    "types-and-universes",
37    "effects",
38    "attributes",
39    "macros-and-notation",
40    "proof-terms-and-tactics",
41    "surface-expansion-elaboration-traces",
42    "project-context-and-dependencies",
43];
44
45/// Source range in UTF-8 byte offsets.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
47pub struct ProgramRange {
48    start: usize,
49    end: usize,
50}
51
52impl ProgramRange {
53    /// Creates an exact UTF-8 byte range.
54    #[must_use]
55    pub const fn new(start: usize, end: usize) -> Self {
56        Self { start, end }
57    }
58
59    /// Inclusive start byte.
60    #[must_use]
61    pub const fn start(self) -> usize {
62        self.start
63    }
64
65    /// Exclusive end byte.
66    #[must_use]
67    pub const fn end(self) -> usize {
68        self.end
69    }
70}
71
72/// One lexical scope.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct ProgramScope {
75    id: String,
76    parent: Option<String>,
77    range: ProgramRange,
78    depth: usize,
79}
80
81impl ProgramScope {
82    /// Stable scope identifier.
83    #[must_use]
84    pub fn id(&self) -> &str {
85        &self.id
86    }
87
88    /// Parent scope identifier.
89    #[must_use]
90    pub fn parent(&self) -> Option<&str> {
91        self.parent.as_deref()
92    }
93
94    /// Source range governed by the scope.
95    #[must_use]
96    pub const fn range(&self) -> ProgramRange {
97        self.range
98    }
99
100    /// Lexical nesting depth.
101    #[must_use]
102    pub const fn depth(&self) -> usize {
103        self.depth
104    }
105}
106
107/// A resolved declaration and its exact references.
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct ProgramBinding {
110    id: String,
111    name: String,
112    kind: String,
113    scope: String,
114    declaration: ProgramRange,
115    references: Vec<ProgramRange>,
116}
117
118impl ProgramBinding {
119    /// Stable binding identifier derived from language and declaration byte.
120    #[must_use]
121    pub fn id(&self) -> &str {
122        &self.id
123    }
124
125    /// Declared spelling.
126    #[must_use]
127    pub fn name(&self) -> &str {
128        &self.name
129    }
130
131    /// Declaration kind.
132    #[must_use]
133    pub fn kind(&self) -> &str {
134        &self.kind
135    }
136
137    /// Governing lexical scope.
138    #[must_use]
139    pub fn scope(&self) -> &str {
140        &self.scope
141    }
142
143    /// Declaration source range.
144    #[must_use]
145    pub const fn declaration(&self) -> ProgramRange {
146        self.declaration
147    }
148
149    /// References resolving to this declaration.
150    #[must_use]
151    pub fn references(&self) -> &[ProgramRange] {
152        &self.references
153    }
154}
155
156/// Generic semantic fact with source evidence.
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct ProgramFact {
159    kind: String,
160    name: String,
161    range: ProgramRange,
162    phase: Option<String>,
163    file: Option<String>,
164}
165
166impl ProgramFact {
167    fn new(kind: impl Into<String>, name: impl Into<String>, range: ProgramRange) -> Self {
168        Self {
169            kind: kind.into(),
170            name: name.into(),
171            range,
172            phase: None,
173            file: None,
174        }
175    }
176
177    fn with_phase(mut self, phase: &str) -> Self {
178        self.phase = Some(phase.to_string());
179        self
180    }
181
182    /// Fact kind.
183    #[must_use]
184    pub fn kind(&self) -> &str {
185        &self.kind
186    }
187
188    /// Fact name.
189    #[must_use]
190    pub fn name(&self) -> &str {
191        &self.name
192    }
193
194    /// Source range.
195    #[must_use]
196    pub const fn range(&self) -> ProgramRange {
197        self.range
198    }
199
200    /// Representation phase, when applicable.
201    #[must_use]
202    pub fn phase(&self) -> Option<&str> {
203        self.phase.as_deref()
204    }
205
206    /// The project file the range belongs to, when it is not the analyzed
207    /// program itself.
208    #[must_use]
209    pub fn file(&self) -> Option<&str> {
210        self.file.as_deref()
211    }
212}
213
214/// CST node to semantic fact source mapping.
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct ProgramSourceMapping {
217    link_id: u64,
218    term: String,
219    range: ProgramRange,
220}
221
222impl ProgramSourceMapping {
223    /// Source CST link id.
224    #[must_use]
225    pub const fn link_id(&self) -> u64 {
226        self.link_id
227    }
228
229    /// Grammar term.
230    #[must_use]
231    pub fn term(&self) -> &str {
232        &self.term
233    }
234
235    /// Mapped source range.
236    #[must_use]
237    pub const fn range(&self) -> ProgramRange {
238        self.range
239    }
240}
241
242/// Parse diagnostic retained in the representation.
243#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct ProgramDiagnostic {
245    kind: &'static str,
246    term: String,
247    range: ProgramRange,
248}
249
250impl ProgramDiagnostic {
251    /// Diagnostic kind.
252    #[must_use]
253    pub const fn kind(&self) -> &'static str {
254        self.kind
255    }
256
257    /// Grammar term carrying the diagnostic.
258    #[must_use]
259    pub fn term(&self) -> &str {
260        &self.term
261    }
262
263    /// Diagnostic source range.
264    #[must_use]
265    pub const fn range(&self) -> ProgramRange {
266        self.range
267    }
268}
269
270/// Representation state for one required semantic construct.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub enum ProgramConstructStatus {
273    /// The source contains the construct and it has structured evidence.
274    Represented,
275    /// The source contains no instance of the construct.
276    NotPresent,
277    /// The language does not define this construct class.
278    NotApplicable,
279    /// The runtime has not produced the required semantic structure.
280    Unavailable,
281}
282
283/// One semantic construct coverage record.
284#[derive(Clone, Debug, PartialEq, Eq)]
285pub struct ProgramConstruct {
286    kind: &'static str,
287    status: ProgramConstructStatus,
288    evidence: Vec<ProgramFact>,
289    rationale: Option<String>,
290}
291
292impl ProgramConstruct {
293    /// Construct category.
294    #[must_use]
295    pub const fn kind(&self) -> &'static str {
296        self.kind
297    }
298
299    /// Representation state.
300    #[must_use]
301    pub const fn status(&self) -> ProgramConstructStatus {
302        self.status
303    }
304
305    /// Source-backed evidence.
306    #[must_use]
307    pub fn evidence(&self) -> &[ProgramFact] {
308        &self.evidence
309    }
310
311    /// Rationale for a non-represented state.
312    #[must_use]
313    pub fn rationale(&self) -> Option<&str> {
314        self.rationale.as_deref()
315    }
316}
317
318/// Semantic analysis or binding-safe edit failure.
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum ProgramRepresentationError {
321    /// No four-language frontend is registered.
322    UnsupportedLanguage(String),
323    /// Binding id is absent.
324    UnknownBinding(String),
325    /// Replacement is not a valid identifier.
326    InvalidIdentifier {
327        language: String,
328        identifier: String,
329    },
330    /// Rename would capture another binding.
331    CaptureConflict { identifier: String, offset: usize },
332    /// A range is reversed, out of bounds, or splits a UTF-8 code point.
333    InvalidRange { start: usize, end: usize },
334    /// A move destination falls strictly inside the moved range.
335    DestinationInsideRange,
336    /// The edited result does not parse cleanly.
337    InvalidEdit,
338    /// A serialized program snapshot is malformed or inconsistent.
339    InvalidSnapshot(String),
340}
341
342impl fmt::Display for ProgramRepresentationError {
343    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
344        match self {
345            Self::UnsupportedLanguage(language) => {
346                write!(
347                    formatter,
348                    "program semantics are not registered for {language}"
349                )
350            }
351            Self::UnknownBinding(binding) => write!(formatter, "unknown binding {binding}"),
352            Self::InvalidIdentifier {
353                language,
354                identifier,
355            } => write!(
356                formatter,
357                "{identifier:?} is not a valid {language} identifier"
358            ),
359            Self::CaptureConflict { identifier, offset } => write!(
360                formatter,
361                "rename would capture {identifier} at {offset} (binding conflict)"
362            ),
363            Self::InvalidRange { start, end } => {
364                write!(formatter, "invalid UTF-8 source range {start}..{end}")
365            }
366            Self::DestinationInsideRange => {
367                formatter.write_str("move destination is inside the moved range")
368            }
369            Self::InvalidEdit => formatter.write_str("structured edit does not reparse cleanly"),
370            Self::InvalidSnapshot(reason) => {
371                write!(formatter, "invalid program snapshot: {reason}")
372            }
373        }
374    }
375}
376
377impl Error for ProgramRepresentationError {}
378
379/// CST-backed resolved program model.
380#[derive(Clone, Debug)]
381pub struct ProgramRepresentation {
382    schema_version: u32,
383    language: &'static str,
384    source: String,
385    project: ProgramProjectContext,
386    network: LinkNetwork,
387    scopes: Vec<ProgramScope>,
388    bindings: Vec<ProgramBinding>,
389    unresolved_references: Vec<ProgramFact>,
390    source_mappings: Vec<ProgramSourceMapping>,
391    modules: Vec<ProgramFact>,
392    types: Vec<ProgramFact>,
393    extensions: Vec<ProgramFact>,
394    proofs: Vec<ProgramFact>,
395    diagnostics: Vec<ProgramDiagnostic>,
396    project_modules: Vec<ProgramProjectModule>,
397    project_facts: Vec<ProgramFact>,
398    project_references: Vec<ProgramProjectReference>,
399    expansions: Vec<ProgramExpansion>,
400    constructs: Vec<ProgramConstruct>,
401}
402
403impl ProgramRepresentation {
404    fn analyze(
405        source: &str,
406        language: &str,
407        project: ProgramProjectContext,
408    ) -> Result<Self, ProgramRepresentationError> {
409        Self::analyze_decorated(source, language, project, &DecoratorSet::empty())
410    }
411
412    fn analyze_decorated(
413        source: &str,
414        language: &str,
415        project: ProgramProjectContext,
416        decorators: &DecoratorSet,
417    ) -> Result<Self, ProgramRepresentationError> {
418        let support = language_support(language)
419            .ok_or_else(|| ProgramRepresentationError::UnsupportedLanguage(language.to_string()))?;
420        let network = LinkNetwork::parse(source, support.name, ParseConfiguration::default());
421        let source_mappings = decorate_syntax_facts(syntax_facts(&network), source, decorators);
422        let tokens = semantic_tokens(source, support.name, &source_mappings);
423        let (scopes, bindings, unresolved_references) =
424            resolve_bindings(&tokens, &source_mappings, source.len(), support.name);
425        let context = project::project_semantics(
426            support.name,
427            source,
428            &source_mappings,
429            &bindings,
430            &unresolved_references,
431            &project,
432        );
433        let modules = module_facts(
434            &tokens,
435            &source_mappings,
436            source,
437            support.name,
438            &project,
439            &context.requests,
440        );
441        let types = type_facts(&tokens, &source_mappings, support.name);
442        let extensions = extension_facts(&tokens, &source_mappings, source, support.name);
443        let proofs = proof_facts(&tokens, &source_mappings, support.name);
444        let mut diagnostics = diagnostic_facts(&network);
445        diagnostics.extend(project_diagnostics(&modules, &context.resolved_requests));
446        diagnostics.extend(context.diagnostics.iter().cloned());
447        let mut program = Self {
448            schema_version: PROGRAM_REPRESENTATION_SCHEMA_VERSION,
449            language: support.name,
450            source: source.to_string(),
451            project,
452            network,
453            scopes,
454            bindings,
455            unresolved_references,
456            source_mappings,
457            modules,
458            types,
459            extensions,
460            proofs,
461            diagnostics,
462            project_modules: context.modules.clone(),
463            project_facts: context.facts.clone(),
464            project_references: context.references.clone(),
465            expansions: context.expansions.clone(),
466            constructs: Vec::new(),
467        };
468        program.constructs = construct_facts(&program, &context);
469        Ok(program)
470    }
471
472    /// Schema revision.
473    #[must_use]
474    pub const fn schema_version(&self) -> u32 {
475        self.schema_version
476    }
477
478    /// Canonical language name.
479    #[must_use]
480    pub const fn language(&self) -> &'static str {
481        self.language
482    }
483
484    /// Exact source text.
485    #[must_use]
486    pub fn source(&self) -> &str {
487        &self.source
488    }
489
490    /// Project context used during analysis.
491    #[must_use]
492    pub const fn project(&self) -> &ProgramProjectContext {
493        &self.project
494    }
495
496    /// Concrete syntax network enriched by this representation.
497    #[must_use]
498    pub const fn network(&self) -> &LinkNetwork {
499        &self.network
500    }
501
502    /// Lexical scopes inferred from parsed source.
503    #[must_use]
504    pub fn scopes(&self) -> &[ProgramScope] {
505        &self.scopes
506    }
507
508    /// Local bindings inferred from parsed source.
509    #[must_use]
510    pub fn bindings(&self) -> &[ProgramBinding] {
511        &self.bindings
512    }
513
514    /// Identifier occurrences without a local declaration.
515    #[must_use]
516    pub fn unresolved_references(&self) -> &[ProgramFact] {
517        &self.unresolved_references
518    }
519
520    /// CST source mappings.
521    #[must_use]
522    pub fn source_mappings(&self) -> &[ProgramSourceMapping] {
523        &self.source_mappings
524    }
525
526    /// Module and import facts.
527    #[must_use]
528    pub fn modules(&self) -> &[ProgramFact] {
529        &self.modules
530    }
531
532    /// Type and universe facts.
533    #[must_use]
534    pub fn types(&self) -> &[ProgramFact] {
535        &self.types
536    }
537
538    /// Attribute, macro, notation, and syntax-extension facts.
539    #[must_use]
540    pub fn extensions(&self) -> &[ProgramFact] {
541        &self.extensions
542    }
543
544    /// Proof and tactic facts.
545    #[must_use]
546    pub fn proofs(&self) -> &[ProgramFact] {
547        &self.proofs
548    }
549
550    /// Parse diagnostics.
551    #[must_use]
552    pub fn diagnostics(&self) -> &[ProgramDiagnostic] {
553        &self.diagnostics
554    }
555
556    /// Project modules the entry program's requests resolve to.
557    #[must_use]
558    pub fn project_modules(&self) -> &[ProgramProjectModule] {
559        &self.project_modules
560    }
561
562    /// Project files and manifests read during analysis.
563    #[must_use]
564    pub fn project_facts(&self) -> &[ProgramFact] {
565        &self.project_facts
566    }
567
568    /// Links from the entry program to declarations in other project files.
569    #[must_use]
570    pub fn project_references(&self) -> &[ProgramProjectReference] {
571        &self.project_references
572    }
573
574    /// Macro, template and notation expansions enabled by the project.
575    #[must_use]
576    pub fn expansions(&self) -> &[ProgramExpansion] {
577        &self.expansions
578    }
579
580    /// Semantic construct coverage.
581    #[must_use]
582    pub fn constructs(&self) -> &[ProgramConstruct] {
583        &self.constructs
584    }
585
586    /// Emits exact source from the retained token network.
587    #[must_use]
588    pub fn emit(&self) -> String {
589        self.network.reconstruct_text()
590    }
591}
592
593/// Analyzes a four-language program with project context.
594pub fn analyze_program(
595    source: &str,
596    language: &str,
597    project: ProgramProjectContext,
598) -> Result<ProgramRepresentation, ProgramRepresentationError> {
599    ProgramRepresentation::analyze(source, language, project)
600}
601
602/// [`analyze_program`] after the `cst-to-ast` decorators of `decorators`.
603///
604/// They see each syntax node the model is built from as `{ term, text }`:
605/// setting `term` changes the construct the semantics read it as, and `drop`
606/// leaves it out of the model. The network keeps the parsed tree either way.
607pub fn analyze_program_decorated(
608    source: &str,
609    language: &str,
610    project: ProgramProjectContext,
611    decorators: &DecoratorSet,
612) -> Result<ProgramRepresentation, ProgramRepresentationError> {
613    ProgramRepresentation::analyze_decorated(source, language, project, decorators)
614}
615
616// The syntax facts the `cst-to-ast` decorators leave, with the terms they set.
617fn decorate_syntax_facts(
618    facts: Vec<ProgramSourceMapping>,
619    source: &str,
620    decorators: &DecoratorSet,
621) -> Vec<ProgramSourceMapping> {
622    if !decorators.has(DecoratorLevel::CstToAst) {
623        return facts;
624    }
625    facts
626        .into_iter()
627        .filter_map(|mut fact| {
628            let text = source
629                .get(fact.range.start..fact.range.end)
630                .unwrap_or_default();
631            let record = decorator_record([("term", fact.term.as_str()), ("text", text)]);
632            let decorated = decorators.decorate(DecoratorLevel::CstToAst, &record)?;
633            record_field(&decorated, "term").clone_into(&mut fact.term);
634            Some(fact)
635        })
636        .collect()
637}
638
639/// Constructs a program and rejects syntax that cannot be represented cleanly.
640pub fn construct_program(
641    source: &str,
642    language: &str,
643    project: ProgramProjectContext,
644) -> Result<ProgramRepresentation, ProgramRepresentationError> {
645    let program = ProgramRepresentation::analyze(source, language, project)?;
646    if !program.network.verify_full_match(None).is_clean() {
647        return Err(ProgramRepresentationError::InvalidEdit);
648    }
649    Ok(program)
650}
651
652#[derive(Clone, Copy, Debug, PartialEq, Eq)]
653enum TokenKind {
654    Identifier,
655    Keyword,
656    Punctuation,
657}
658
659fn module_facts(
660    tokens: &[SemanticToken],
661    syntax: &[ProgramSourceMapping],
662    source: &str,
663    language: &str,
664    project: &ProgramProjectContext,
665    project_requests: &[ProgramFact],
666) -> Vec<ProgramFact> {
667    let markers = module_markers(language);
668    let mut facts = Vec::new();
669    for (index, token) in tokens.iter().enumerate() {
670        if !markers.contains(&token.text.as_str()) {
671            continue;
672        }
673        let names = tokens[index + 1..]
674            .iter()
675            .take_while(|candidate| {
676                candidate.text != ";" && !markers.contains(&candidate.text.as_str())
677            })
678            .filter(|candidate| candidate.kind == TokenKind::Identifier)
679            .take(3)
680            .map(|candidate| candidate.text.as_str())
681            .collect::<Vec<_>>()
682            .join(".");
683        facts.push(ProgramFact::new(&token.text, names, token.range));
684    }
685    facts.extend(project.dependencies().iter().map(|dependency| {
686        ProgramFact::new(
687            "declared-project-dependency",
688            dependency,
689            ProgramRange::default(),
690        )
691    }));
692    let requests = module_requests(syntax, source, language);
693    for request in requests.into_iter().chain(project_requests.iter().cloned()) {
694        if project_has_module(project, &request.name, language) {
695            facts.push(ProgramFact::new(
696                "recognized-toolchain-module",
697                &request.name,
698                request.range,
699            ));
700        }
701        facts.push(request);
702    }
703    unique_facts(facts)
704}
705
706fn type_facts(
707    tokens: &[SemanticToken],
708    syntax: &[ProgramSourceMapping],
709    language: &str,
710) -> Vec<ProgramFact> {
711    let mut facts = syntax
712        .iter()
713        .filter(|mapping| {
714            let term = mapping.term.to_ascii_lowercase();
715            term.contains("type") || term.contains("universe")
716        })
717        .map(|mapping| {
718            ProgramFact::new("syntax-type", &mapping.term, mapping.range).with_phase("surface")
719        })
720        .collect::<Vec<_>>();
721    for (index, token) in tokens.iter().enumerate() {
722        if token.text == ":"
723            && let Some(value) = tokens.get(index + 1)
724        {
725            facts.push(
726                ProgramFact::new("annotation", &value.text, value.range).with_phase("surface"),
727            );
728        }
729        if matches!(token.text.as_str(), "universe" | "Universe" | "Type") {
730            facts.push(
731                ProgramFact::new(
732                    "universe",
733                    tokens
734                        .get(index + 1)
735                        .map_or(&token.text, |value| &value.text),
736                    token.range,
737                )
738                .with_phase("surface"),
739            );
740        }
741    }
742    if language == "JavaScript" {
743        facts.push(
744            ProgramFact::new("dynamic-type", "ECMAScript value", ProgramRange::default())
745                .with_phase("runtime"),
746        );
747    }
748    unique_facts(facts)
749}
750
751fn extension_facts(
752    tokens: &[SemanticToken],
753    syntax: &[ProgramSourceMapping],
754    source: &str,
755    language: &str,
756) -> Vec<ProgramFact> {
757    let markers = extension_markers(language);
758    let mut facts = tokens
759        .iter()
760        .filter(|token| markers.contains(&token.text.as_str()))
761        .map(|token| ProgramFact::new(&token.text, &token.text, token.range))
762        .collect::<Vec<_>>();
763    facts.extend(syntax.iter().filter_map(|mapping| {
764        let term = mapping.term.to_ascii_lowercase();
765        [
766            "macro",
767            "attribute",
768            "decorator",
769            "quotation",
770            "template",
771            "notation",
772        ]
773        .iter()
774        .any(|needle| term.contains(needle))
775        .then(|| ProgramFact::new(&mapping.term, &mapping.term, mapping.range))
776    }));
777    if language == "JavaScript"
778        && let Some(start) = source.find("\"use strict\"")
779    {
780        facts.push(ProgramFact::new(
781            "directive",
782            "use strict",
783            ProgramRange::new(start, start + 12),
784        ));
785    }
786    unique_facts(facts)
787}
788
789fn proof_facts(
790    tokens: &[SemanticToken],
791    syntax: &[ProgramSourceMapping],
792    language: &str,
793) -> Vec<ProgramFact> {
794    if matches!(language, "JavaScript" | "Rust") {
795        return Vec::new();
796    }
797    let markers = proof_markers(language);
798    let facts = tokens
799        .iter()
800        .filter(|token| markers.contains(&token.text.as_str()))
801        .map(|token| ProgramFact::new(&token.text, &token.text, token.range))
802        .chain(syntax.iter().filter_map(|mapping| {
803            let term = mapping.term.to_ascii_lowercase();
804            ["theorem", "proof", "tactic"]
805                .iter()
806                .any(|needle| term.contains(needle))
807                .then(|| ProgramFact::new(&mapping.term, &mapping.term, mapping.range))
808        }))
809        .collect();
810    unique_facts(facts)
811}
812
813fn diagnostic_facts(network: &LinkNetwork) -> Vec<ProgramDiagnostic> {
814    network
815        .verify_full_match(None)
816        .issues()
817        .iter()
818        .filter_map(|issue| {
819            let link = network.link(issue.link_id())?;
820            let span = link.metadata().span();
821            Some(ProgramDiagnostic {
822                kind: if link.metadata().flags().is_missing() {
823                    "missing"
824                } else {
825                    "parse-error"
826                },
827                term: link.metadata().term().unwrap_or_default().to_string(),
828                range: span.map_or_else(ProgramRange::default, |span| {
829                    ProgramRange::new(span.byte_range().start(), span.byte_range().end())
830                }),
831            })
832        })
833        .collect()
834}
835
836fn project_diagnostics(
837    modules: &[ProgramFact],
838    resolved: &[(String, usize)],
839) -> Vec<ProgramDiagnostic> {
840    let recognized = modules
841        .iter()
842        .filter(|fact| fact.kind == "recognized-toolchain-module")
843        .map(|fact| fact.name.as_str())
844        .collect::<std::collections::HashSet<_>>();
845    modules
846        .iter()
847        .filter(|fact| {
848            fact.kind == "module-import"
849                && !recognized.contains(fact.name.as_str())
850                && !resolved
851                    .iter()
852                    .any(|(name, start)| *name == fact.name && *start == fact.range.start)
853        })
854        .map(|fact| ProgramDiagnostic {
855            kind: "missing-project-context",
856            term: fact.name.clone(),
857            range: fact.range,
858        })
859        .collect()
860}
861
862fn construct_facts(
863    program: &ProgramRepresentation,
864    context: &project::ProjectResult,
865) -> Vec<ProgramConstruct> {
866    SEMANTIC_CONSTRUCTS
867        .iter()
868        .map(|kind| {
869            let mut evidence = construct_evidence(program, kind);
870            evidence.extend(project::project_evidence(&program.project, context, kind));
871            if evidence.is_empty() && *kind == "surface-expansion-elaboration-traces" {
872                return ProgramConstruct {
873                    kind,
874                    status: ProgramConstructStatus::Unavailable,
875                    evidence,
876                    rationale: Some(
877                        "no macro expansion or elaboration trace has been produced".to_string(),
878                    ),
879                };
880            }
881            if evidence.is_empty()
882                && *kind == "proof-terms-and-tactics"
883                && matches!(program.language, "JavaScript" | "Rust")
884            {
885                return ProgramConstruct {
886                    kind,
887                    status: ProgramConstructStatus::NotApplicable,
888                    evidence,
889                    rationale: Some(format!(
890                        "{} defines no proof/tactic sublanguage",
891                        program.language
892                    )),
893                };
894            }
895            let represented = !evidence.is_empty();
896            ProgramConstruct {
897                kind,
898                status: if represented {
899                    ProgramConstructStatus::Represented
900                } else {
901                    ProgramConstructStatus::NotPresent
902                },
903                evidence,
904                rationale: (!represented).then(|| {
905                    "the analyzed source contains no instance of this construct".to_string()
906                }),
907            }
908        })
909        .collect()
910}
911
912fn construct_evidence(program: &ProgramRepresentation, kind: &str) -> Vec<ProgramFact> {
913    match kind {
914        "modules-and-imports" => program.modules.clone(),
915        "scopes-and-bindings" => program
916            .bindings
917            .iter()
918            .map(|binding| ProgramFact::new(&binding.kind, &binding.name, binding.declaration))
919            .collect(),
920        "recursive-definitions" => program
921            .bindings
922            .iter()
923            .filter(|binding| {
924                matches!(
925                    binding.kind.as_str(),
926                    "function" | "Fixpoint" | "CoFixpoint" | "def"
927                ) && !binding.references.is_empty()
928            })
929            .map(|binding| ProgramFact::new("recursive", &binding.name, binding.declaration))
930            .collect(),
931        "types-and-universes" => program.types.clone(),
932        "effects" => effect_facts(program),
933        "attributes" => program
934            .extensions
935            .iter()
936            .filter(|fact| {
937                let kind = fact.kind.to_ascii_lowercase();
938                ["attribute", "directive", "allow", "local", "simp", "#"]
939                    .iter()
940                    .any(|needle| kind.contains(needle))
941            })
942            .cloned()
943            .collect(),
944        "macros-and-notation" => program
945            .extensions
946            .iter()
947            .filter(|fact| {
948                let kind = fact.kind.to_ascii_lowercase();
949                [
950                    "macro", "notation", "template", "tagged", "prefix", "postfix", "infix",
951                    "syntax",
952                ]
953                .iter()
954                .any(|needle| kind.contains(needle))
955            })
956            .cloned()
957            .collect(),
958        "proof-terms-and-tactics" => program.proofs.clone(),
959        "project-context-and-dependencies" => program
960            .project
961            .files()
962            .iter()
963            .chain(program.project.dependencies())
964            .map(|name| ProgramFact::new("project", name, ProgramRange::default()))
965            .collect(),
966        _ => Vec::new(),
967    }
968}
969
970fn effect_facts(program: &ProgramRepresentation) -> Vec<ProgramFact> {
971    let mut facts = Vec::new();
972    for marker in effect_markers(program.language) {
973        facts.extend(program.source.match_indices(marker).map(|(start, value)| {
974            ProgramFact::new(
975                "effect",
976                value,
977                ProgramRange::new(start, start + value.len()),
978            )
979        }));
980    }
981    unique_facts(facts)
982}