diff --git a/cranelift/src/libfilecheck/checker.rs b/cranelift/src/libfilecheck/checker.rs index ea23f9e0d9..eba5da78a7 100644 --- a/cranelift/src/libfilecheck/checker.rs +++ b/cranelift/src/libfilecheck/checker.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::cmp::max; use std::fmt::{self, Display, Formatter}; use MatchRange; +use explain::{Recorder, Explainer}; // The different kinds of directives we support. enum Directive { @@ -146,12 +147,24 @@ impl Checker { /// This returns `true` if the text matches all the directives, `false` if it doesn't. /// An error is only returned if there is a problem with the directives. pub fn check(&self, text: &str, vars: &VariableMap) -> Result { - let mut state = State::new(text, vars); + self.run(text, vars, &mut ()) + } + + /// Explain how directives are matched against the input text. + pub fn explain(&self, text: &str, vars: &VariableMap) -> Result<(bool, String)> { + let mut expl = Explainer::new(text); + let success = try!(self.run(text, vars, &mut expl)); + expl.finish(); + Ok((success, expl.to_string())) + } + + fn run(&self, text: &str, vars: &VariableMap, recorder: &mut Recorder) -> Result { + let mut state = State::new(text, vars, recorder); // For each pending `not:` check, store (begin-offset, regex). let mut nots = Vec::new(); - for dct in &self.directives { + for (dct_idx, dct) in self.directives.iter().enumerate() { let (pat, range) = match *dct { Directive::Check(ref pat) => (pat, state.check()), Directive::SameLn(ref pat) => (pat, state.sameln()), @@ -164,7 +177,7 @@ impl Checker { // The `not:` directives test the same range as `unordered:` directives. In // particular, if they refer to defined variables, their range is restricted to // the text following the match that defined the variable. - nots.push((state.unordered_begin(pat), try!(pat.resolve(&state)))); + nots.push((dct_idx, state.unordered_begin(pat), try!(pat.resolve(&state)))); continue; } Directive::Regex(ref var, ref rx) => { @@ -177,6 +190,7 @@ impl Checker { } }; // Check if `pat` matches in `range`. + state.recorder.directive(dct_idx); if let Some((match_begin, match_end)) = try!(state.match_positive(pat, range)) { if let &Directive::Unordered(_) = dct { // This was an unordered unordered match. @@ -188,11 +202,14 @@ impl Checker { state.max_match = match_end; // Verify any pending `not:` directives now that we know their range. - for (not_begin, rx) in nots.drain(..) { - if let Some(_) = rx.find(&text[not_begin..match_begin]) { + for (not_idx, not_begin, rx) in nots.drain(..) { + state.recorder.directive(not_idx); + if let Some((s, e)) = rx.find(&text[not_begin..match_begin]) { // Matched `not:` pattern. - // TODO: Use matched range for an error message. + state.recorder.matched_not(rx.as_str(), (not_begin + s, not_begin + e)); return Ok(false); + } else { + state.recorder.missed_not(rx.as_str(), (not_begin, match_begin)); } } } @@ -203,7 +220,8 @@ impl Checker { } // Verify any pending `not:` directives after the last ordered directive. - for (not_begin, rx) in nots.drain(..) { + for (not_idx, not_begin, rx) in nots.drain(..) { + state.recorder.directive(not_idx); if let Some(_) = rx.find(&text[not_begin..]) { // Matched `not:` pattern. // TODO: Use matched range for an error message. @@ -224,8 +242,10 @@ pub struct VarDef { } struct State<'a> { - env_vars: &'a VariableMap, text: &'a str, + env_vars: &'a VariableMap, + recorder: &'a mut Recorder, + vars: HashMap, // Offset after the last ordered match. This does not include recent unordered matches. last_ordered: usize, @@ -234,10 +254,11 @@ struct State<'a> { } impl<'a> State<'a> { - fn new(text: &'a str, env_vars: &'a VariableMap) -> State<'a> { + fn new(text: &'a str, env_vars: &'a VariableMap, recorder: &'a mut Recorder) -> State<'a> { State { text: text, env_vars: env_vars, + recorder: recorder, vars: HashMap::new(), last_ordered: 0, max_match: 0, @@ -309,8 +330,10 @@ impl<'a> State<'a> { rx.captures(txt).map(|caps| { let matched_range = caps.pos(0).expect("whole expression must match"); for var in defs { + let txtval = caps.name(var).unwrap_or(""); + self.recorder.defined_var(var, txtval); let vardef = VarDef { - value: Value::Text(caps.name(var).unwrap_or("").to_string()), + value: Value::Text(txtval.to_string()), // This offset is the end of the whole matched pattern, not just the text // defining the variable. offset: range.0 + matched_range.1, @@ -320,7 +343,14 @@ impl<'a> State<'a> { matched_range }) }; - Ok(matched_range.map(|(b, e)| (range.0 + b, range.0 + e))) + Ok(if let Some((b, e)) = matched_range { + let r = (range.0 + b, range.0 + e); + self.recorder.matched_check(rx.as_str(), r); + Some(r) + } else { + self.recorder.missed_check(rx.as_str(), range); + None + }) } } diff --git a/cranelift/src/libfilecheck/explain.rs b/cranelift/src/libfilecheck/explain.rs new file mode 100644 index 0000000000..53dd4003a7 --- /dev/null +++ b/cranelift/src/libfilecheck/explain.rs @@ -0,0 +1,196 @@ +//! Explaining how *filecheck* matched or failed to match a file. + +use MatchRange; +use std::fmt::{self, Display, Formatter}; +use std::cmp::min; + +/// Record events during matching. +pub trait Recorder { + /// Set the directive we're talking about now. + fn directive(&mut self, dct: usize); + + /// Matched a positive check directive (check/sameln/nextln/unordered). + fn matched_check(&mut self, regex: &str, matched: MatchRange); + + /// Matched a `not:` directive. This means the match will fail. + fn matched_not(&mut self, regex: &str, matched: MatchRange); + + /// Missed a positive check directive. The range given is the range searched for a match. + fn missed_check(&mut self, regex: &str, searched: MatchRange); + + /// Missed `not:` directive (as intended). + fn missed_not(&mut self, regex: &str, searched: MatchRange); + + /// The directive defined a variable. + fn defined_var(&mut self, varname: &str, value: &str); +} + +/// The null recorder just doesn't listen to anything you say. +impl Recorder for () { + fn directive(&mut self, _: usize) {} + fn matched_check(&mut self, _: &str, _: MatchRange) {} + fn matched_not(&mut self, _: &str, _: MatchRange) {} + fn defined_var(&mut self, _: &str, _: &str) {} + fn missed_check(&mut self, _: &str, _: MatchRange) {} + fn missed_not(&mut self, _: &str, _: MatchRange) {} +} + +struct Match { + directive: usize, + is_match: bool, + is_not: bool, + regex: String, + range: MatchRange, +} + +struct VarDef { + directive: usize, + varname: String, + value: String, +} + +/// Record an explanation for the matching process, success or failure. +pub struct Explainer<'a> { + text: &'a str, + directive: usize, + matches: Vec, + vardefs: Vec, +} + +impl<'a> Explainer<'a> { + pub fn new(text: &'a str) -> Explainer { + Explainer { + text: text, + directive: 0, + matches: Vec::new(), + vardefs: Vec::new(), + } + } + + /// Finish up after recording all events in a match. + pub fn finish(&mut self) { + self.matches.sort_by_key(|m| (m.range, m.directive)); + self.vardefs.sort_by_key(|v| v.directive); + } +} + +impl<'a> Display for Explainer<'a> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + // Offset of beginning of the last line printed. + let mut curln = 0; + // Offset of beginning of the next line to be printed. + let mut nextln = 0; + + for m in &self.matches { + // Emit lines until m.range.0 is visible. + while nextln <= m.range.0 && nextln < self.text.len() { + let newln = self.text[nextln..] + .find('\n') + .map(|d| nextln + d + 1) + .unwrap_or(self.text.len()); + assert!(newln > nextln); + try!(writeln!(f, "> {}", &self.text[nextln..newln - 1])); + curln = nextln; + nextln = newln; + } + + // Emit ~~~ under the part of the match in curln. + if m.is_match { + try!(write!(f, " ")); + let mend = min(m.range.1, nextln - 1); + for pos in curln..mend { + try!(if pos < m.range.0 { + write!(f, " ") + } else if pos == m.range.0 { + write!(f, "^") + } else { + write!(f, "~") + }); + } + try!(writeln!(f, "")); + } + + // Emit the match message itself. + try!(writeln!(f, + "{} #{}{}: {}", + if m.is_match { "Matched" } else { "Missed" }, + m.directive, + if m.is_not { " not" } else { "" }, + m.regex)); + + // Emit any variable definitions. + if let Ok(found) = self.vardefs.binary_search_by_key(&m.directive, |v| v.directive) { + let mut first = found; + while first > 0 && self.vardefs[first - 1].directive == m.directive { + first -= 1; + } + for d in &self.vardefs[first..] { + if d.directive != m.directive { + break; + } + try!(writeln!(f, "Define {}={}", d.varname, d.value)); + } + } + } + + // Emit trailing lines. + for line in self.text[nextln..].lines() { + try!(writeln!(f, "> {}", line)); + } + Ok(()) + } +} + +impl<'a> Recorder for Explainer<'a> { + fn directive(&mut self, dct: usize) { + self.directive = dct; + } + + fn matched_check(&mut self, regex: &str, matched: MatchRange) { + self.matches.push(Match { + directive: self.directive, + is_match: true, + is_not: false, + regex: regex.to_owned(), + range: matched, + }); + } + + fn matched_not(&mut self, regex: &str, matched: MatchRange) { + self.matches.push(Match { + directive: self.directive, + is_match: true, + is_not: true, + regex: regex.to_owned(), + range: matched, + }); + } + + fn missed_check(&mut self, regex: &str, searched: MatchRange) { + self.matches.push(Match { + directive: self.directive, + is_match: false, + is_not: false, + regex: regex.to_owned(), + range: searched, + }); + } + + fn missed_not(&mut self, regex: &str, searched: MatchRange) { + self.matches.push(Match { + directive: self.directive, + is_match: false, + is_not: true, + regex: regex.to_owned(), + range: searched, + }); + } + + fn defined_var(&mut self, varname: &str, value: &str) { + self.vardefs.push(VarDef { + directive: self.directive, + varname: varname.to_owned(), + value: value.to_owned(), + }); + } +} diff --git a/cranelift/src/libfilecheck/lib.rs b/cranelift/src/libfilecheck/lib.rs index 8d61a1dfbf..f6c53c5dcf 100644 --- a/cranelift/src/libfilecheck/lib.rs +++ b/cranelift/src/libfilecheck/lib.rs @@ -244,6 +244,7 @@ mod error; mod variable; mod pattern; mod checker; +mod explain; /// The range of a match in the input text. pub type MatchRange = (usize, usize); diff --git a/cranelift/src/tools/rsfilecheck.rs b/cranelift/src/tools/rsfilecheck.rs index bce661282c..b64a86283e 100644 --- a/cranelift/src/tools/rsfilecheck.rs +++ b/cranelift/src/tools/rsfilecheck.rs @@ -20,10 +20,21 @@ pub fn run(files: Vec, verbose: bool) -> CommandResult { let mut buffer = String::new(); try!(io::stdin().read_to_string(&mut buffer).map_err(|e| format!("stdin: {}", e))); - if try!(checker.check(&buffer, NO_VARIABLES).map_err(|e| e.to_string())) { + if verbose { + let (success, explain) = try!(checker.explain(&buffer, NO_VARIABLES) + .map_err(|e| e.to_string())); + print!("{}", explain); + if success { + println!("OK"); + Ok(()) + } else { + Err("Check failed".to_string()) + } + } else if try!(checker.check(&buffer, NO_VARIABLES).map_err(|e| e.to_string())) { Ok(()) } else { - // TODO: We need to do better than this. + let (_, explain) = try!(checker.explain(&buffer, NO_VARIABLES).map_err(|e| e.to_string())); + print!("{}", explain); Err("Check failed".to_string()) } } diff --git a/cranelift/tests/cfg/loop.cton b/cranelift/tests/cfg/loop.cton index 07d93926d8..fd6e4fa6b9 100644 --- a/cranelift/tests/cfg/loop.cton +++ b/cranelift/tests/cfg/loop.cton @@ -2,12 +2,14 @@ function nonsense(i32, i32) -> f32 { ; check: digraph nonsense { +; regex: I=\binst\d+\b +; check: label="{ebb0 | <$(BRZ=$I)>brz ebb2 | <$(JUMP=$I)>jump ebb1}"] ebb0(v1: i32, v2: i32): v3 = f64const 0x0.0 - brz v2, ebb2 ; unordered: ebb0:inst1 -> ebb2 + brz v2, ebb2 ; unordered: ebb0:$BRZ -> ebb2 v4 = iconst.i32 0 - jump ebb1(v4) ; unordered: ebb0:inst3 -> ebb1 + jump ebb1(v4) ; unordered: ebb0:$JUMP -> ebb1 ebb1(v5: i32): v6 = imul_imm v5, 4