1use crate::ast::DeclBody;
5use crate::checker::check_module;
6use crate::conformance::judge_corpus;
7use crate::fmt::format;
8use crate::module::{load_modules, run_universe, Bind, LoadResult, Module};
9use crate::package::{open_package_universe, verify_lock};
10use crate::semantics::{json_str, read_json, Diag};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::rc::Rc;
14
15fn usage() -> i32 {
16 eprintln!("usage:\n decl --version\n decl check <file>... [--json]\n decl evaluate <file> [--input name=doc.json]... [--output name[=file]]... [--json]\n decl validate <dir>\n decl validate <file> [--input name=doc.json]... [--expect-errors E1,E2] [--json]\n decl fmt <file>... [--check]\n decl repl [file.decl] [--input name=doc.json]... [--script session.txt | --script -] [--compact]");
17 2
18}
19
20pub fn open_universe(file: &str) -> LoadResult {
23 let abs = std::path::absolute(file).unwrap_or_else(|_| PathBuf::from(file));
24 let pkg = open_package_universe(&abs);
25 let mut diags: Vec<Diag> = vec![];
26 if let Some(u) = &pkg {
27 diags.extend(u.diags.clone());
28 diags.extend(verify_lock(u));
29 }
30 let r = load_modules(&abs, pkg.as_ref().map(|u| &u.resolver), None);
31 diags.extend(r.diags);
32 LoadResult {
33 modules: r.modules,
34 entry: r.entry,
35 diags,
36 }
37}
38
39fn print_diag(file: &str, d: &Diag, json: bool, collected: &mut Vec<String>) {
40 if json {
41 collected.push(d.to_json(Some(file)));
42 return;
43 }
44 eprintln!(
45 "{file}: {}{}{}{}: {}",
46 d.severity,
47 d.code
48 .as_ref()
49 .map(|c| format!(" [{c}]"))
50 .unwrap_or_default(),
51 d.id.as_ref().map(|i| format!(" {i}")).unwrap_or_default(),
52 if d.path.is_empty() {
53 String::new()
54 } else {
55 format!(" at {}", d.path)
56 },
57 d.message
58 );
59}
60
61pub fn input_binds(
69 modules: &[Rc<Module>],
70 specs: &[String],
71) -> Result<Vec<Bind>, (i32, Option<Diag>)> {
72 let doc_error = |name: &str, message: String| -> (i32, Option<Diag>) {
73 (
74 1,
75 Some(Diag {
76 severity: "error".into(),
77 id: None,
78 message,
79 path: name.to_string(),
80 code: Some("E6004".into()),
81 loc: None,
82 by: None,
83 }),
84 )
85 };
86 let mut binds = vec![];
87 for spec in specs {
88 let Some((name, file)) = spec.split_once('=') else {
89 eprintln!("--input expects name=doc.json, got {spec}");
90 return Err((2, None));
91 };
92 let Some(module) = modules
93 .iter()
94 .find(|m| m.env.inputs.borrow().contains_key(name))
95 else {
96 eprintln!("no input named {name}");
97 return Err((2, None));
98 };
99 let text = match std::fs::read_to_string(file) {
100 Ok(t) => t,
101 Err(_) => {
102 return Err(doc_error(
103 name,
104 format!("bound document cannot be read: {file}"),
105 ))
106 }
107 };
108 let raw = match read_json(&text) {
109 Ok(v) => v,
110 Err(_) => {
111 return Err(doc_error(
112 name,
113 format!("bound document is not well-formed JSON: {file}"),
114 ));
115 }
116 };
117 binds.push(Bind {
118 module: Some(module.clone()),
119 input: name.to_string(),
120 raw,
121 });
122 }
123 Ok(binds)
124}
125
126pub fn evaluate(
134 file: &str,
135 outputs: &[String],
136 inputs: &[String],
137) -> (i32, Option<String>, Vec<(String, Diag)>, Vec<String>) {
138 let tag = |ds: Vec<Diag>| -> Vec<(String, Diag)> {
139 ds.into_iter().map(|d| (file.to_string(), d)).collect()
140 };
141 let mut targets: Vec<(String, Option<String>)> = vec![];
142 for spec in outputs {
143 let (name, dest) = match spec.split_once('=') {
144 Some((n, f)) => (n.to_string(), Some(f.to_string())),
145 None => (spec.clone(), None),
146 };
147 if name.is_empty() || dest.as_deref() == Some("") {
148 eprintln!("--output expects name or name=file, got {spec}");
149 return (2, None, vec![], vec![]);
150 }
151 targets.push((name, dest));
152 }
153 if targets.iter().filter(|(_, f)| f.is_none()).count() > 1 {
154 eprintln!("--output: at most one document can go to stdout");
155 return (2, None, vec![], vec![]);
156 }
157 let r = open_universe(file);
158 let Some(entry) = r.entry else {
159 return (1, None, tag(r.diags), vec![]);
160 };
161 if !r.diags.is_empty() {
162 return (1, None, tag(r.diags), vec![]);
163 }
164 let checks: Vec<(String, Diag)> = r
165 .modules
166 .iter()
167 .flat_map(|m| {
168 let path = file_tag(file, Some(entry.path.as_path()), &m.path);
169 check_module(&m.decls, Some(m.env.clone()), None)
170 .into_iter()
171 .map(move |d| (path.clone(), d))
172 .collect::<Vec<_>>()
173 })
174 .collect();
175 if !checks.is_empty() {
176 return (1, None, checks, vec![]);
177 }
178 let binds = match input_binds(&r.modules, inputs) {
179 Ok(b) => b,
180 Err((code, diag)) => {
181 return (
182 code,
183 None,
184 diag.into_iter().map(|d| (file.to_string(), d)).collect(),
185 vec![],
186 )
187 }
188 };
189 let (eng, diags) = run_universe(&r.modules, &entry, binds);
190 if diags.iter().any(|d| d.severity == "error") {
191 return (1, None, tag(diags), vec![]);
192 }
193 let names: Vec<String> = if targets.is_empty() {
194 entry
195 .decls
196 .iter()
197 .filter(|d| d.exported)
198 .filter_map(|d| match &d.body {
199 DeclBody::Output { name, .. } => Some(name.clone()),
200 _ => None,
201 })
202 .collect()
203 } else {
204 targets.iter().map(|(n, _)| n.clone()).collect()
205 };
206 let mut notes = vec![];
207 for n in &names {
208 if entry.env.root(n).is_none() {
209 notes.push(format!("no root named {n}"));
210 }
211 }
212 if !notes.is_empty() {
213 return (1, None, tag(diags), notes);
214 }
215 let doc = |n: &str| eng.serialize(&entry.env.root(n).unwrap(), n, false);
216 let mut text = None;
217 if targets.is_empty() {
218 if names.is_empty() {
219 notes.push(format!(
220 "{file}: exports no output; --output <name> selects a root"
221 ));
222 }
223 text = Some(format!(
224 "{{{}}}",
225 names
226 .iter()
227 .map(|n| format!("{}:{}", json_str(n), doc(n)))
228 .collect::<Vec<_>>()
229 .join(",")
230 ));
231 } else {
232 for (n, dest) in &targets {
233 match dest {
234 None => text = Some(doc(n)),
235 Some(path) => {
236 if std::fs::write(path, doc(n) + "\n").is_err() {
237 notes.push(format!("cannot write {path}"));
238 return (1, None, tag(diags), notes);
239 }
240 }
241 }
242 }
243 }
244 (0, text, tag(diags), notes)
245}
246
247pub fn validate_file(file: &str, inputs: &[String]) -> Result<Vec<(String, Diag)>, i64> {
253 let r = open_universe(file);
254 let mut diags: Vec<(String, Diag)> = r
255 .diags
256 .iter()
257 .map(|d| (file.to_string(), d.clone()))
258 .collect();
259 let Some(entry) = r.entry else {
260 return Ok(diags);
261 };
262 if !diags.is_empty() {
263 return Ok(diags);
264 }
265 let checks: Vec<(String, Diag)> = r
266 .modules
267 .iter()
268 .flat_map(|m| {
269 let path = file_tag(file, Some(entry.path.as_path()), &m.path);
270 check_module(&m.decls, Some(m.env.clone()), None)
271 .into_iter()
272 .map(move |d| (path.clone(), d))
273 .collect::<Vec<_>>()
274 })
275 .collect();
276 if !checks.is_empty() {
277 return Ok(checks);
278 }
279 let binds = match input_binds(&r.modules, inputs) {
280 Ok(b) => b,
281 Err((_, Some(d))) => return Ok(vec![(file.to_string(), d)]),
282 Err((code, None)) => return Err(-(code as i64)),
283 };
284 diags.extend(
285 run_universe(&r.modules, &entry, binds)
286 .1
287 .into_iter()
288 .map(|d| (file.to_string(), d)),
289 );
290 Ok(diags)
291}
292
293pub fn check_files(paths: &[String]) -> Vec<(String, Diag)> {
296 let mut out = vec![];
297 for f in paths {
298 let r = open_universe(f);
299 out.extend(r.diags.into_iter().map(|d| (f.clone(), d)));
300 for m in &r.modules {
301 let path = file_tag(f, r.entry.as_ref().map(|e| e.path.as_path()), &m.path);
302 out.extend(
303 check_module(&m.decls, Some(m.env.clone()), None)
304 .into_iter()
305 .map(|d| (path.clone(), d)),
306 );
307 }
308 }
309 out
310}
311
312pub fn file_tag(given: &str, entry: Option<&Path>, module: &Path) -> String {
315 if entry == Some(module) {
316 given.to_string()
317 } else {
318 module.display().to_string()
319 }
320}
321
322pub fn main(args: Vec<String>) -> i32 {
324 let Some(cmd) = args.first().cloned() else {
325 return usage();
326 };
327 if cmd == "--version" {
329 println!("decl {}", env!("CARGO_PKG_VERSION"));
330 return 0;
331 }
332 if cmd == "repl" {
334 return crate::repl::run_repl(args[1..].to_vec());
335 }
336 let mut flags: HashMap<String, String> = HashMap::new();
337 let mut input_flags: Vec<String> = vec![]; let mut output_flags: Vec<String> = vec![]; let mut pos: Vec<String> = vec![];
340 let mut i = 1;
341 while i < args.len() {
342 let a = &args[i];
343 if let Some(name) = a.strip_prefix("--") {
344 if ["output", "input", "expect-errors"].contains(&name)
345 && i + 1 < args.len()
346 && !args[i + 1].starts_with("--")
347 {
348 if name == "input" {
349 input_flags.push(args[i + 1].clone());
350 } else if name == "output" {
351 output_flags.push(args[i + 1].clone());
352 } else {
353 flags.insert(name.to_string(), args[i + 1].clone());
354 }
355 i += 2;
356 continue;
357 }
358 flags.insert(name.to_string(), "true".into());
359 } else {
360 pos.push(a.clone());
361 }
362 i += 1;
363 }
364 let json = flags.contains_key("json");
365 let mut collected: Vec<String> = vec![];
366 match cmd.as_str() {
367 "check" => {
368 if pos.is_empty() {
369 return usage();
370 }
371 let diags = check_files(&pos);
372 for (file, d) in &diags {
373 print_diag(file, d, json, &mut collected);
374 }
375 if diags.is_empty() {
376 eprintln!("ok: {} entry file(s) check clean", pos.len());
377 }
378 if json {
379 println!("[{}]", collected.join(","));
380 }
381 if diags.is_empty() {
382 0
383 } else {
384 1
385 }
386 }
387 "evaluate" => {
388 let Some(f) = pos.first() else { return usage() };
389 let (code, text, diags, notes) = evaluate(f, &output_flags, &input_flags);
390 if code == 2 {
391 return 2; }
393 for (file, d) in &diags {
394 print_diag(file, d, json, &mut collected);
395 }
396 for n in ¬es {
397 eprintln!("{n}");
398 }
399 if json {
400 println!(
401 "{{\"ok\":{},\"value\":{},\"diagnostics\":[{}]}}",
402 code == 0,
403 text.clone().unwrap_or_else(|| "null".into()),
404 collected.join(",")
405 );
406 } else if let Some(t) = text {
407 println!("{t}");
408 }
409 code
410 }
411 "validate" => {
412 let Some(target) = pos.first() else {
413 return usage();
414 };
415 let tp = Path::new(target);
416 if tp.is_dir() {
417 let abs = std::path::absolute(tp).unwrap_or_else(|_| tp.to_path_buf());
418 let (mut ok, mut fail) = (0, 0);
419 for v in judge_corpus(&abs) {
420 if v.ok {
421 ok += 1;
422 } else {
423 fail += 1;
424 eprintln!("FAIL {} {}", v.file.display(), v.detail);
425 }
426 }
427 eprintln!("{ok} ok, {fail} failed");
428 if fail > 0 {
429 1
430 } else {
431 0
432 }
433 } else {
434 let diags = match validate_file(target, &input_flags) {
435 Ok(d) => d,
436 Err(n) => return (-n) as i32,
437 };
438 for (file, d) in &diags {
439 print_diag(file, d, json, &mut collected);
440 }
441 if json {
442 println!("[{}]", collected.join(","));
443 }
444 let err_codes: Vec<String> = diags
445 .iter()
446 .filter(|(_, d)| d.severity == "error")
447 .map(|(_, d)| d.code.clone().unwrap_or_default())
448 .collect();
449 if let Some(expect) = flags.get("expect-errors") {
450 let want: Vec<String> = expect
451 .split(',')
452 .map(|w| w.trim().to_string())
453 .filter(|w| !w.is_empty())
454 .collect();
455 let missing: Vec<&String> =
456 want.iter().filter(|w| !err_codes.contains(w)).collect();
457 let extra: Vec<&String> =
458 err_codes.iter().filter(|c| !want.contains(c)).collect();
459 if !missing.is_empty() || !extra.is_empty() {
460 if !missing.is_empty() {
461 eprintln!(
462 "expected error(s) not reported: {}",
463 missing
464 .iter()
465 .map(|s| s.as_str())
466 .collect::<Vec<_>>()
467 .join(", ")
468 );
469 }
470 if !extra.is_empty() {
471 eprintln!(
472 "unexpected error(s): {}",
473 extra
474 .iter()
475 .map(|s| s.as_str())
476 .collect::<Vec<_>>()
477 .join(", ")
478 );
479 }
480 return 1;
481 }
482 eprintln!(
483 "ok: expected errors reported ({})",
484 if want.is_empty() {
485 "none".to_string()
486 } else {
487 want.join(", ")
488 }
489 );
490 return 0;
491 }
492 if err_codes.is_empty() {
493 0
494 } else {
495 1
496 }
497 }
498 }
499 "fmt" => {
500 if pos.is_empty() {
501 return usage();
502 }
503 let (mut changed, mut bad) = (0, 0);
504 for f in &pos {
505 let Ok(src) = std::fs::read_to_string(f) else {
506 eprintln!("{f}: cannot be read");
507 bad += 1;
508 continue;
509 };
510 let out = match format(&src) {
511 Ok(o) => o,
512 Err(e) => {
513 eprintln!("{f}: {e}");
514 bad += 1;
515 continue;
516 }
517 };
518 if out != src {
519 changed += 1;
520 if flags.contains_key("check") {
521 eprintln!("would reformat {f}");
522 } else {
523 let _ = std::fs::write(f, out);
524 eprintln!("reformatted {f}");
525 }
526 }
527 }
528 if bad > 0 || (flags.contains_key("check") && changed > 0) {
529 1
530 } else {
531 0
532 }
533 }
534 _ => usage(),
535 }
536}