1use std::cell::{Ref, RefCell, RefMut};
3use std::fmt::Write;
4use std::ops::DerefMut;
5use std::time::{Duration, Instant};
6use std::{fs, io};
7
8use crate::tt::*;
9use crate::zero::NameSegment;
10
11use fnotation::parser::Prec;
12use fnotation::{FNtnTop, ParseConfig};
13use scopeguard::guard;
14use tattle::display::SourceInfo;
15use tattle::{Reporter, declare_error};
16use text_elab::*;
17use toplevel::*;
18
19const PARSE_CONFIG: ParseConfig = ParseConfig::new(
20 &[
21 (":", Prec::nonassoc(20)),
22 (":=", Prec::nonassoc(10)),
23 ("&", Prec::lassoc(40)),
24 ("*", Prec::lassoc(60)),
25 ],
26 &[":", ":=", "&", "Unit", "Id", "*"],
27 &["type", "def", "syn", "chk", "norm", "generate", "set_theory"],
28);
29
30declare_error!(TOP_ERROR, "top", "an error at the top-level");
31
32pub enum BatchOutput {
34 Snapshot(RefCell<String>),
36 Interactive,
38}
39
40impl BatchOutput {
41 fn report(&self, reporter: &Reporter, source_info: &SourceInfo) {
42 match self {
43 BatchOutput::Snapshot(out) => source_info
44 .extract_report_to(
45 RefMut::deref_mut(&mut out.borrow_mut()),
46 reporter.clone(),
47 tattle::display::DisplayOptions::String,
48 )
49 .unwrap(),
50 BatchOutput::Interactive => {
51 source_info
52 .extract_report_to_io(
53 &mut io::stdout(),
54 reporter.clone(),
55 tattle::display::DisplayOptions::Terminal,
56 )
57 .unwrap();
58 }
59 }
60 }
61
62 fn log_input(&self, src: &str, decl: &FNtnTop) {
63 match self {
64 BatchOutput::Snapshot(out) => {
65 writeln!(out.borrow_mut(), "{}", decl.loc.slice(src)).unwrap();
66 }
67 BatchOutput::Interactive => {}
68 }
69 }
70
71 fn declared(&self, name: NameSegment) {
72 match self {
73 BatchOutput::Snapshot(out) => {
74 writeln!(out.borrow_mut(), "#/ declared: {}", name).unwrap();
75 }
76 BatchOutput::Interactive => {}
77 }
78 }
79
80 fn got_result(&self, result: &str) {
81 match self {
82 BatchOutput::Snapshot(out) => {
83 writeln!(out.borrow_mut(), "#/ result: {}", result).unwrap();
84 }
85 BatchOutput::Interactive => {
86 println!("{}", result);
87 }
88 }
89 }
90
91 fn display_errors(&self, should_fail: bool, reporter: &Reporter, source_info: &SourceInfo) {
92 match self {
93 BatchOutput::Snapshot(out) => {
94 let mut out = out.borrow_mut();
95 if reporter.errored() {
96 if should_fail {
97 writeln!(out, "#/ expected errors:").unwrap();
98 } else {
99 writeln!(out, "#/ unexpected errors:").unwrap();
100 }
101 let mut errors = String::new();
102 source_info
103 .extract_report_to(
104 &mut errors,
105 reporter.clone(),
106 tattle::display::DisplayOptions::String,
107 )
108 .unwrap();
109 for l in errors.lines() {
110 writeln!(out, "#/ {l}").unwrap();
111 }
112 }
113 writeln!(out).unwrap();
114 }
115 BatchOutput::Interactive => {
116 if should_fail {
117 reporter.poll();
118 } else {
119 self.report(reporter, source_info);
120 }
121 }
122 }
123 }
124
125 fn record_time(&self, path: &str, elapsed_t: Duration) {
126 match self {
127 BatchOutput::Snapshot(_) => {}
128 BatchOutput::Interactive => {
129 println!(
130 "finished elaborating {} in {}ms",
131 path,
132 elapsed_t.as_micros() as f64 / 1000.0
133 );
134 }
135 }
136 }
137
138 #[allow(unused)]
139 pub fn result<'a>(&'a self) -> Ref<'a, String> {
141 match self {
142 BatchOutput::Snapshot(out) => out.borrow(),
143 _ => panic!("cannot get result of interactive session"),
144 }
145 }
146}
147
148pub fn run(path: &str, output: &BatchOutput) -> io::Result<bool> {
150 let src = match fs::read_to_string(path) {
151 Ok(s) => s,
152 Err(e) => {
153 eprintln!("Could not read {}: {}", &path, e);
154 return Ok(false);
155 }
156 };
157 elaborate(&src, path, output)
158}
159
160pub fn elaborate(src: &str, path: &str, output: &BatchOutput) -> io::Result<bool> {
162 let reporter = Reporter::new();
163 let source_info = SourceInfo::new(Some(path), src);
164 let start_t = Instant::now();
165 let _unwind_guard = guard((), |_| {
166 output.report(&reporter, &source_info);
167 });
168 let mut succeeded = true;
169 let _ = PARSE_CONFIG.with_parsed_top(src, reporter.clone(), |topntns| {
170 let mut toplevel = Toplevel::new(std_theories());
171 let mut topelab = TopElaborator::new(reporter.clone());
172 for topntn in topntns.iter() {
173 output.log_input(src, topntn);
174 let mut should_fail = false;
175 for annot in topntn.annotations {
176 #[allow(clippy::single_match)]
179 match annot.ast0() {
180 fnotation::Var("should_fail") => {
181 should_fail = true;
182 }
183 _ => {}
184 }
185 }
186 if let Some(d) = topelab.elab(&toplevel, topntn) {
187 if should_fail {
188 reporter.error(
189 topntn.loc,
190 TOP_ERROR,
191 "expected a failure to elaborate".to_string(),
192 );
193 } else {
194 match d {
195 TopElabResult::Declaration(name_segment, top_decl) => {
196 toplevel.declarations.insert(name_segment, top_decl);
197 output.declared(name_segment);
198 }
199 TopElabResult::Output(s) => {
200 output.got_result(&s);
201 }
202 }
203 }
204 } else if !should_fail {
205 succeeded = false;
206 }
207 output.display_errors(should_fail, &reporter, &source_info);
208 }
209 Some(())
210 });
211 output.record_time(path, Instant::now() - start_t);
212 Ok(succeeded)
213}
214
215#[test]
216fn snapshot_examples() {
217 use similar::{ChangeTag, TextDiff};
218 let mut succeeded = true;
219 for f in fs::read_dir("examples").unwrap() {
220 let Ok(f) = f else {
221 continue;
222 };
223 let os_fname = f.file_name();
224 let fname = os_fname.to_str().unwrap();
225 if !fname.ends_with(".dbltt") {
226 continue;
227 }
228 let output = BatchOutput::Snapshot(RefCell::new(String::new()));
229 succeeded = run(f.path().to_str().unwrap(), &output).unwrap() && succeeded;
230 let golden_path = format!("examples/{}.snapshot", fname);
231 if matches!(std::env::var("UPDATE_SNAPSHOT"), Ok(s) if &s == "1") {
232 fs::write(&golden_path, output.result().as_str()).unwrap();
233 } else {
234 let golden = fs::read_to_string(&golden_path).unwrap_or_default();
235 let result = output.result();
236 let result_str = result.as_str();
237 if &golden != result_str {
238 succeeded = false;
239 println!("failed snapshot test for examples/{fname}:");
240 let diff = TextDiff::from_lines(golden.as_str(), result_str);
241
242 for change in diff.iter_all_changes() {
243 match change.tag() {
244 ChangeTag::Delete => {
245 print!("- {}", change);
246 }
247 ChangeTag::Insert => {
248 print!("+ {}", change);
249 }
250 ChangeTag::Equal => {}
251 };
252 }
253 }
254 }
255 }
256 assert!(succeeded);
257}