use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use crate::checker::check_module;
use crate::cli::{check_files, file_tag, open_universe};
use crate::module::{run_universe, Bind, Module};
use crate::parse::parse_source;
use crate::pipeline::run_pipeline;
pub use crate::pipeline::{evaluate_source, Report};
use crate::semantics::{read_json, Diag};
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub file: String,
pub code: Option<String>,
pub id: Option<String>,
pub severity: String,
pub message: String,
pub path: String,
}
#[derive(Clone, Debug)]
pub struct DeclError {
pub message: String,
pub diagnostics: Vec<Diagnostic>,
}
impl fmt::Display for DeclError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for DeclError {}
#[derive(Clone, Debug)]
pub enum Document {
File(PathBuf),
Json(String),
}
#[derive(Clone, Debug, Default)]
pub struct EvaluateOptions {
pub inputs: Vec<(String, Document)>,
pub outputs: Vec<String>,
}
fn tagged(file: &str, d: &Diag) -> Diagnostic {
Diagnostic {
file: file.to_string(),
code: d.code.clone(),
id: d.id.clone(),
severity: d.severity.clone(),
message: d.message.clone(),
path: d.path.clone(),
}
}
fn fail<T>(fallback: &str, diagnostics: Vec<Diagnostic>) -> Result<T, DeclError> {
let message = diagnostics
.first()
.map(|d| d.message.clone())
.unwrap_or_else(|| fallback.to_string());
Err(DeclError {
message,
diagnostics,
})
}
fn bind_inputs(
modules: &[Rc<Module>],
file: &str,
inputs: &[(String, Document)],
) -> Result<Vec<Bind>, DeclError> {
let mut binds = vec![];
for (name, doc) in inputs {
let Some(module) = modules
.iter()
.find(|m| m.env.inputs.borrow().contains_key(name))
else {
return Err(DeclError {
message: format!("no input named {name}"),
diagnostics: vec![],
});
};
let e6004 = |message: String| Diagnostic {
file: file.to_string(),
code: Some("E6004".into()),
id: None,
severity: "error".into(),
message,
path: name.clone(),
};
let (text, place) = match doc {
Document::File(p) => match std::fs::read_to_string(p) {
Ok(t) => (t, p.display().to_string()),
Err(_) => {
return fail(
"",
vec![e6004(format!(
"bound document cannot be read: {}",
p.display()
))],
)
}
},
Document::Json(t) => (t.clone(), name.clone()),
};
let raw = match read_json(&text) {
Ok(v) => v,
Err(_) => {
return fail(
"",
vec![e6004(format!(
"bound document is not well-formed JSON: {place}"
))],
)
}
};
binds.push(Bind {
module: Some(module.clone()),
input: name.clone(),
raw,
});
}
Ok(binds)
}
pub fn evaluate(path: &str, opts: &EvaluateOptions) -> Result<BTreeMap<String, String>, DeclError> {
let r = open_universe(path);
let Some(entry) = r.entry.clone() else {
return fail(
&format!("{path}: cannot be loaded"),
r.diags.iter().map(|d| tagged(path, d)).collect(),
);
};
if !r.diags.is_empty() {
return fail("", r.diags.iter().map(|d| tagged(path, d)).collect());
}
let checks: Vec<Diagnostic> = r
.modules
.iter()
.flat_map(|m| {
let tag = file_tag(path, Some(entry.path.as_path()), &m.path);
check_module(&m.decls, Some(m.env.clone()), None)
.iter()
.map(|d| tagged(&tag, d))
.collect::<Vec<_>>()
})
.collect();
if !checks.is_empty() {
return fail("", checks);
}
let binds = bind_inputs(&r.modules, path, &opts.inputs)?;
let (eng, diags) = run_universe(&r.modules, &entry, binds);
let report: Vec<Diagnostic> = diags.iter().map(|d| tagged(path, d)).collect();
if report.iter().any(|d| d.severity == "error") {
return fail("", report);
}
let names: Vec<String> = if opts.outputs.is_empty() {
entry
.decls
.iter()
.filter(|d| d.exported)
.filter_map(|d| match &d.body {
crate::ast::DeclBody::Output { name, .. } => Some(name.clone()),
_ => None,
})
.collect()
} else {
opts.outputs.clone()
};
let mut out = BTreeMap::new();
for n in &names {
let Some(v) = entry.env.root(n) else {
return Err(DeclError {
message: format!("no root named {n}"),
diagnostics: report,
});
};
out.insert(n.clone(), eng.serialize(&v, n, false));
}
Ok(out)
}
pub fn check(paths: &[&str]) -> Vec<Diagnostic> {
let owned: Vec<String> = paths.iter().map(|p| p.to_string()).collect();
check_files(&owned)
.iter()
.map(|(file, d)| tagged(file, d))
.collect()
}
pub fn validate(path: &str, inputs: &[(String, Document)]) -> Result<Vec<Diagnostic>, DeclError> {
let src = std::fs::read_to_string(path).map_err(|_| DeclError {
message: format!("{path}: cannot be read"),
diagnostics: vec![],
})?;
let parsed = parse_source(&src);
if !parsed.errors.is_empty() {
return Err(DeclError {
message: format!("{path}: {} parse error(s)", parsed.errors.len()),
diagnostics: vec![],
});
}
let checks: Vec<Diagnostic> = check_module(&parsed.decls, None, None)
.iter()
.map(|d| tagged(path, d))
.collect();
if !checks.is_empty() {
return Ok(checks);
}
if !inputs.is_empty() {
let r = open_universe(path);
let Some(entry) = r.entry.clone() else {
return fail(
&format!("{path}: cannot be loaded"),
r.diags.iter().map(|d| tagged(path, d)).collect(),
);
};
let binds = bind_inputs(&r.modules, path, inputs)?;
let (_, diags) = run_universe(&r.modules, &entry, binds);
return Ok(diags.iter().map(|d| tagged(path, d)).collect());
}
Ok(run_pipeline(&parsed.decls)
.diags
.iter()
.map(|d| tagged(path, d))
.collect())
}
pub fn format_source(text: &str) -> Result<String, DeclError> {
crate::fmt::format(text).map_err(|message| DeclError {
message,
diagnostics: vec![],
})
}