1use std::collections::BTreeMap;
8use std::fmt;
9use std::path::PathBuf;
10
11use crate::checker::check_module;
12use crate::cli::{check_files, file_tag, open_universe};
13use crate::module::{run_universe, Bind, Module};
14use crate::parse::parse_source;
15use crate::pipeline::run_pipeline;
16pub use crate::pipeline::{evaluate_source, Report};
17use crate::semantics::{read_json, Diag};
18use std::rc::Rc;
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct Diagnostic {
23 pub file: String,
24 pub code: Option<String>,
25 pub id: Option<String>,
26 pub severity: String,
27 pub message: String,
28 pub path: String,
29}
30
31#[derive(Clone, Debug)]
34pub struct DeclError {
35 pub message: String,
36 pub diagnostics: Vec<Diagnostic>,
37}
38impl fmt::Display for DeclError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.write_str(&self.message)
41 }
42}
43impl std::error::Error for DeclError {}
44
45#[derive(Clone, Debug)]
47pub enum Document {
48 File(PathBuf),
49 Json(String),
50}
51
52#[derive(Clone, Debug, Default)]
54pub struct EvaluateOptions {
55 pub inputs: Vec<(String, Document)>,
57 pub outputs: Vec<String>,
60}
61
62fn tagged(file: &str, d: &Diag) -> Diagnostic {
63 Diagnostic {
64 file: file.to_string(),
65 code: d.code.clone(),
66 id: d.id.clone(),
67 severity: d.severity.clone(),
68 message: d.message.clone(),
69 path: d.path.clone(),
70 }
71}
72fn fail<T>(fallback: &str, diagnostics: Vec<Diagnostic>) -> Result<T, DeclError> {
73 let message = diagnostics
74 .first()
75 .map(|d| d.message.clone())
76 .unwrap_or_else(|| fallback.to_string());
77 Err(DeclError {
78 message,
79 diagnostics,
80 })
81}
82
83fn bind_inputs(
85 modules: &[Rc<Module>],
86 file: &str,
87 inputs: &[(String, Document)],
88) -> Result<Vec<Bind>, DeclError> {
89 let mut binds = vec![];
90 for (name, doc) in inputs {
91 let Some(module) = modules
92 .iter()
93 .find(|m| m.env.inputs.borrow().contains_key(name))
94 else {
95 return Err(DeclError {
96 message: format!("no input named {name}"),
97 diagnostics: vec![],
98 });
99 };
100 let e6004 = |message: String| Diagnostic {
101 file: file.to_string(),
102 code: Some("E6004".into()),
103 id: None,
104 severity: "error".into(),
105 message,
106 path: name.clone(),
107 };
108 let (text, place) = match doc {
109 Document::File(p) => match std::fs::read_to_string(p) {
110 Ok(t) => (t, p.display().to_string()),
111 Err(_) => {
112 return fail(
113 "",
114 vec![e6004(format!(
115 "bound document cannot be read: {}",
116 p.display()
117 ))],
118 )
119 }
120 },
121 Document::Json(t) => (t.clone(), name.clone()),
122 };
123 let raw = match read_json(&text) {
124 Ok(v) => v,
125 Err(_) => {
126 return fail(
127 "",
128 vec![e6004(format!(
129 "bound document is not well-formed JSON: {place}"
130 ))],
131 )
132 }
133 };
134 binds.push(Bind {
135 module: Some(module.clone()),
136 input: name.clone(),
137 raw,
138 });
139 }
140 Ok(binds)
141}
142
143pub fn evaluate(path: &str, opts: &EvaluateOptions) -> Result<BTreeMap<String, String>, DeclError> {
147 let r = open_universe(path);
148 let Some(entry) = r.entry.clone() else {
149 return fail(
150 &format!("{path}: cannot be loaded"),
151 r.diags.iter().map(|d| tagged(path, d)).collect(),
152 );
153 };
154 if !r.diags.is_empty() {
155 return fail("", r.diags.iter().map(|d| tagged(path, d)).collect());
156 }
157 let checks: Vec<Diagnostic> = r
158 .modules
159 .iter()
160 .flat_map(|m| {
161 let tag = file_tag(path, Some(entry.path.as_path()), &m.path);
162 check_module(&m.decls, Some(m.env.clone()), None)
163 .iter()
164 .map(|d| tagged(&tag, d))
165 .collect::<Vec<_>>()
166 })
167 .collect();
168 if !checks.is_empty() {
169 return fail("", checks);
170 }
171 let binds = bind_inputs(&r.modules, path, &opts.inputs)?;
172 let (eng, diags) = run_universe(&r.modules, &entry, binds);
173 let report: Vec<Diagnostic> = diags.iter().map(|d| tagged(path, d)).collect();
174 if report.iter().any(|d| d.severity == "error") {
175 return fail("", report);
176 }
177 let names: Vec<String> = if opts.outputs.is_empty() {
178 entry
179 .decls
180 .iter()
181 .filter(|d| d.exported)
182 .filter_map(|d| match &d.body {
183 crate::ast::DeclBody::Output { name, .. } => Some(name.clone()),
184 _ => None,
185 })
186 .collect()
187 } else {
188 opts.outputs.clone()
189 };
190 let mut out = BTreeMap::new();
191 for n in &names {
192 let Some(v) = entry.env.root(n) else {
193 return Err(DeclError {
194 message: format!("no root named {n}"),
195 diagnostics: report,
196 });
197 };
198 out.insert(n.clone(), eng.serialize(&v, n, false));
199 }
200 Ok(out)
201}
202
203pub fn check(paths: &[&str]) -> Vec<Diagnostic> {
205 let owned: Vec<String> = paths.iter().map(|p| p.to_string()).collect();
206 check_files(&owned)
207 .iter()
208 .map(|(file, d)| tagged(file, d))
209 .collect()
210}
211
212pub fn validate(path: &str, inputs: &[(String, Document)]) -> Result<Vec<Diagnostic>, DeclError> {
216 let src = std::fs::read_to_string(path).map_err(|_| DeclError {
217 message: format!("{path}: cannot be read"),
218 diagnostics: vec![],
219 })?;
220 let parsed = parse_source(&src);
221 if !parsed.errors.is_empty() {
222 return Err(DeclError {
223 message: format!("{path}: {} parse error(s)", parsed.errors.len()),
224 diagnostics: vec![],
225 });
226 }
227 let checks: Vec<Diagnostic> = check_module(&parsed.decls, None, None)
228 .iter()
229 .map(|d| tagged(path, d))
230 .collect();
231 if !checks.is_empty() {
232 return Ok(checks);
233 }
234 if !inputs.is_empty() {
235 let r = open_universe(path);
236 let Some(entry) = r.entry.clone() else {
237 return fail(
238 &format!("{path}: cannot be loaded"),
239 r.diags.iter().map(|d| tagged(path, d)).collect(),
240 );
241 };
242 let binds = bind_inputs(&r.modules, path, inputs)?;
243 let (_, diags) = run_universe(&r.modules, &entry, binds);
244 return Ok(diags.iter().map(|d| tagged(path, d)).collect());
245 }
246 Ok(run_pipeline(&parsed.decls)
247 .diags
248 .iter()
249 .map(|d| tagged(path, d))
250 .collect())
251}
252
253pub fn format_source(text: &str) -> Result<String, DeclError> {
255 crate::fmt::format(text).map_err(|message| DeclError {
256 message,
257 diagnostics: vec![],
258 })
259}