mod javascript;
mod lean;
mod rocq;
mod rust_lang;
mod tree;
use std::collections::BTreeMap;
use std::rc::Rc;
use super::analysis::syntax_facts;
use super::{ProgramBinding, ProgramDiagnostic, ProgramFact, ProgramRange};
use crate::{LinkNetwork, ParseConfiguration};
use tree::{SyntaxTree, Target};
const TYPE_KINDS: [&str; 9] = [
"class",
"struct",
"enum",
"trait",
"type",
"structure",
"inductive",
"constructor",
"universe",
];
const EFFECT_TRAITS: [&str; 4] = ["async", "generator", "io", "proof-state"];
const PROOF_KINDS: [&str; 3] = ["theorem", "lemma", "tactic"];
const PROOF_ROLES: [&str; 3] = ["tactic", "assertion", "const-assertion"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProgramProjectSource {
path: String,
source: String,
}
impl ProgramProjectSource {
#[must_use]
pub fn new(path: impl Into<String>, source: impl Into<String>) -> Self {
Self {
path: path.into(),
source: source.into(),
}
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ProgramProjectContext {
root: String,
files: Vec<String>,
dependencies: Vec<String>,
extensions: Vec<String>,
entry: Option<String>,
sources: Vec<ProgramProjectSource>,
}
impl ProgramProjectContext {
#[must_use]
pub fn new(root: impl Into<String>, files: Vec<String>, dependencies: Vec<String>) -> Self {
Self {
root: root.into(),
files,
dependencies,
extensions: Vec::new(),
entry: None,
sources: Vec::new(),
}
}
#[must_use]
pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
self.extensions = extensions;
self
}
#[must_use]
pub fn with_entry(
mut self,
entry: impl Into<String>,
sources: Vec<ProgramProjectSource>,
) -> Self {
self.entry = Some(entry.into());
self.sources = sources;
self
}
#[must_use]
pub fn root(&self) -> &str {
&self.root
}
#[must_use]
pub fn files(&self) -> &[String] {
&self.files
}
#[must_use]
pub fn dependencies(&self) -> &[String] {
&self.dependencies
}
#[must_use]
pub fn extensions(&self) -> &[String] {
&self.extensions
}
#[must_use]
pub fn entry(&self) -> Option<&str> {
self.entry.as_deref()
}
#[must_use]
pub fn sources(&self) -> &[ProgramProjectSource] {
&self.sources
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProgramProjectModule {
request: String,
module: String,
file: String,
range: ProgramRange,
}
impl ProgramProjectModule {
#[must_use]
pub fn request(&self) -> &str {
&self.request
}
#[must_use]
pub fn module(&self) -> &str {
&self.module
}
#[must_use]
pub fn file(&self) -> &str {
&self.file
}
#[must_use]
pub const fn range(&self) -> ProgramRange {
self.range
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProgramProjectReference {
role: &'static str,
name: String,
range: ProgramRange,
symbol: String,
target_kind: String,
traits: Vec<String>,
file: String,
declaration: ProgramRange,
}
impl ProgramProjectReference {
#[must_use]
pub const fn role(&self) -> &'static str {
self.role
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn range(&self) -> ProgramRange {
self.range
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn target_kind(&self) -> &str {
&self.target_kind
}
#[must_use]
pub fn traits(&self) -> &[String] {
&self.traits
}
#[must_use]
pub fn file(&self) -> &str {
&self.file
}
#[must_use]
pub const fn declaration(&self) -> ProgramRange {
self.declaration
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProgramExpansion {
name: String,
kind: &'static str,
range: ProgramRange,
expansion: String,
target: String,
}
impl ProgramExpansion {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn kind(&self) -> &'static str {
self.kind
}
#[must_use]
pub const fn range(&self) -> ProgramRange {
self.range
}
#[must_use]
pub fn expansion(&self) -> &str {
&self.expansion
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct ProjectResult {
pub(super) modules: Vec<ProgramProjectModule>,
pub(super) facts: Vec<ProgramFact>,
pub(super) references: Vec<ProgramProjectReference>,
pub(super) expansions: Vec<ProgramExpansion>,
pub(super) diagnostics: Vec<ProgramDiagnostic>,
pub(super) requests: Vec<ProgramFact>,
pub(super) resolved_requests: Vec<(String, usize)>,
}
pub(super) fn project_semantics(
language: &'static str,
source: &str,
mappings: &[super::ProgramSourceMapping],
bindings: &[ProgramBinding],
unresolved: &[ProgramFact],
project: &ProgramProjectContext,
) -> ProjectResult {
let Some(entry) = project.entry().filter(|entry| !entry.is_empty()) else {
return ProjectResult::default();
};
let mut context = ProjectContext::new(language, entry, source, mappings, project);
context.bindings = bindings;
context.unresolved = unresolved;
match language {
"JavaScript" => javascript::analyze(&mut context),
"Rust" => rust_lang::analyze(&mut context),
"Lean" => lean::analyze(&mut context),
"Rocq" => rocq::analyze(&mut context),
_ => {}
}
for file in &project.sources {
if context
.parsed
.get(&file.path)
.is_some_and(|parsed| !parsed.clean)
{
context.result.diagnostics.push(ProgramDiagnostic {
kind: "project-parse-error",
term: file.path.clone(),
range: ProgramRange::default(),
});
}
}
context.result
}
#[derive(Debug)]
struct ParsedFile {
path: String,
source: String,
tree: SyntaxTree,
clean: bool,
}
struct ProjectContext<'a> {
language: &'static str,
bindings: &'a [ProgramBinding],
unresolved: &'a [ProgramFact],
entry: Rc<ParsedFile>,
sources: BTreeMap<&'a str, &'a str>,
parsed: BTreeMap<String, Rc<ParsedFile>>,
result: ProjectResult,
}
impl<'a> ProjectContext<'a> {
fn new(
language: &'static str,
entry: &str,
source: &str,
mappings: &[super::ProgramSourceMapping],
project: &'a ProgramProjectContext,
) -> Self {
let mut context = Self {
language,
bindings: &[],
unresolved: &[],
entry: Rc::new(ParsedFile {
path: entry.to_string(),
source: source.to_string(),
tree: SyntaxTree::build(mappings, source),
clean: true,
}),
sources: project
.sources
.iter()
.map(|file| (file.path.as_str(), file.source.as_str()))
.collect(),
parsed: BTreeMap::new(),
result: ProjectResult::default(),
};
for file in &project.sources {
context.fact(
"project-file",
&file.path,
&file.path,
(0, file.source.len()),
);
}
context
}
fn has(&self, path: &str) -> bool {
path == self.entry.path || self.sources.contains_key(path)
}
fn text(&self, path: &str) -> Option<&str> {
if path == self.entry.path {
return Some(&self.entry.source);
}
self.sources.get(path).copied()
}
fn load(&mut self, path: &str) -> Option<Rc<ParsedFile>> {
self.load_as(path, self.language)
}
fn load_as(&mut self, path: &str, language: &str) -> Option<Rc<ParsedFile>> {
if path == self.entry.path {
return Some(Rc::clone(&self.entry));
}
let source = *self.sources.get(path)?;
let parsed = self.parsed.entry(path.to_string()).or_insert_with(|| {
let network = LinkNetwork::parse(source, language, ParseConfiguration::default());
Rc::new(ParsedFile {
path: path.to_string(),
source: source.to_string(),
tree: SyntaxTree::build(&syntax_facts(&network), source),
clean: network.verify_full_match(None).is_clean(),
})
});
Some(Rc::clone(parsed))
}
fn fact(&mut self, kind: &str, name: &str, file: &str, range: (usize, usize)) {
let mut fact = ProgramFact::new(kind, name, ProgramRange::new(range.0, range.1));
fact.file = Some(file.to_string());
self.result.facts.push(fact);
}
fn module(&mut self, request: &str, module: &str, file: &str, range: (usize, usize)) {
self.result.modules.push(ProgramProjectModule {
request: request.to_string(),
module: module.to_string(),
file: file.to_string(),
range: ProgramRange::new(range.0, range.1),
});
self.result
.resolved_requests
.push((request.to_string(), range.0));
}
fn request(&mut self, name: &str, range: (usize, usize)) {
self.result.requests.push(ProgramFact::new(
"module-import",
name,
ProgramRange::new(range.0, range.1),
));
}
fn reference(
&mut self,
role: &'static str,
name: &str,
range: (usize, usize),
target: &Target,
) {
self.result.references.push(ProgramProjectReference {
role,
name: name.to_string(),
range: ProgramRange::new(range.0, range.1),
symbol: target.symbol.clone(),
target_kind: target.kind.clone(),
traits: target.traits.clone(),
file: target.file.clone(),
declaration: ProgramRange::new(target.start, target.end),
});
}
fn expansion(
&mut self,
name: &str,
kind: &'static str,
range: (usize, usize),
expansion: String,
target: &str,
) {
self.result.expansions.push(ProgramExpansion {
name: name.to_string(),
kind,
range: ProgramRange::new(range.0, range.1),
expansion,
target: target.to_string(),
});
}
fn diagnose(&mut self, kind: &'static str, term: String, range: (usize, usize)) {
self.result.diagnostics.push(ProgramDiagnostic {
kind,
term,
range: ProgramRange::new(range.0, range.1),
});
}
}
pub(super) fn project_evidence(
project: &ProgramProjectContext,
result: &ProjectResult,
construct: &str,
) -> Vec<ProgramFact> {
let entry = project.entry().unwrap_or_default();
let located = |kind: String, name: &str, range: ProgramRange, file: &str| {
let mut fact = ProgramFact::new(kind, name, range);
fact.file = Some(file.to_string());
fact
};
let references = |predicate: &dyn Fn(&ProgramProjectReference) -> bool| {
result
.references
.iter()
.filter(|reference| predicate(reference))
.map(|reference| {
located(
format!(
"project-reference:{}:{}",
reference.role, reference.target_kind
),
&reference.symbol,
reference.range,
entry,
)
})
.collect::<Vec<_>>()
};
let has_trait = |reference: &ProgramProjectReference, traits: &[&str]| {
reference
.traits
.iter()
.any(|value| traits.contains(&value.as_str()))
};
match construct {
"modules-and-imports" => result
.modules
.iter()
.map(|module| {
located(
"project-module".to_string(),
&module.module,
module.range,
entry,
)
})
.chain(references(&|reference| reference.role == "import"))
.collect(),
"scopes-and-bindings" => references(&|reference| reference.role == "reference"),
"recursive-definitions" => references(&|reference| has_trait(reference, &["recursive"])),
"types-and-universes" => references(&|reference| {
TYPE_KINDS.contains(&reference.target_kind.as_str())
|| has_trait(reference, &["universe-polymorphic"])
}),
"effects" => references(&|reference| has_trait(reference, &EFFECT_TRAITS)),
"attributes" => references(&|reference| reference.role == "attribute"),
"macros-and-notation" => {
references(&|reference| matches!(reference.role, "macro" | "notation" | "template-tag"))
}
"proof-terms-and-tactics" => references(&|reference| {
PROOF_ROLES.contains(&reference.role)
|| PROOF_KINDS.contains(&reference.target_kind.as_str())
}),
"surface-expansion-elaboration-traces" => result
.expansions
.iter()
.map(|expansion| {
located(
format!("expansion:{}", expansion.kind),
&expansion.target,
expansion.range,
entry,
)
})
.collect(),
"project-context-and-dependencies" => result.facts.clone(),
_ => Vec::new(),
}
}