Add an explainer mode to filecheck.
The -c flag to 'cton-util filecheck' will now print out a description of how the directives are matching the input. This explanation is also printed when a match fails.
This commit is contained in:
@@ -6,6 +6,7 @@ use std::collections::HashMap;
|
|||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use MatchRange;
|
use MatchRange;
|
||||||
|
use explain::{Recorder, Explainer};
|
||||||
|
|
||||||
// The different kinds of directives we support.
|
// The different kinds of directives we support.
|
||||||
enum Directive {
|
enum Directive {
|
||||||
@@ -146,12 +147,24 @@ impl Checker {
|
|||||||
/// This returns `true` if the text matches all the directives, `false` if it doesn't.
|
/// 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.
|
/// An error is only returned if there is a problem with the directives.
|
||||||
pub fn check(&self, text: &str, vars: &VariableMap) -> Result<bool> {
|
pub fn check(&self, text: &str, vars: &VariableMap) -> Result<bool> {
|
||||||
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<bool> {
|
||||||
|
let mut state = State::new(text, vars, recorder);
|
||||||
|
|
||||||
// For each pending `not:` check, store (begin-offset, regex).
|
// For each pending `not:` check, store (begin-offset, regex).
|
||||||
let mut nots = Vec::new();
|
let mut nots = Vec::new();
|
||||||
|
|
||||||
for dct in &self.directives {
|
for (dct_idx, dct) in self.directives.iter().enumerate() {
|
||||||
let (pat, range) = match *dct {
|
let (pat, range) = match *dct {
|
||||||
Directive::Check(ref pat) => (pat, state.check()),
|
Directive::Check(ref pat) => (pat, state.check()),
|
||||||
Directive::SameLn(ref pat) => (pat, state.sameln()),
|
Directive::SameLn(ref pat) => (pat, state.sameln()),
|
||||||
@@ -164,7 +177,7 @@ impl Checker {
|
|||||||
// The `not:` directives test the same range as `unordered:` directives. In
|
// The `not:` directives test the same range as `unordered:` directives. In
|
||||||
// particular, if they refer to defined variables, their range is restricted to
|
// particular, if they refer to defined variables, their range is restricted to
|
||||||
// the text following the match that defined the variable.
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
Directive::Regex(ref var, ref rx) => {
|
Directive::Regex(ref var, ref rx) => {
|
||||||
@@ -177,6 +190,7 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Check if `pat` matches in `range`.
|
// 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 Some((match_begin, match_end)) = try!(state.match_positive(pat, range)) {
|
||||||
if let &Directive::Unordered(_) = dct {
|
if let &Directive::Unordered(_) = dct {
|
||||||
// This was an unordered unordered match.
|
// This was an unordered unordered match.
|
||||||
@@ -188,11 +202,14 @@ impl Checker {
|
|||||||
state.max_match = match_end;
|
state.max_match = match_end;
|
||||||
|
|
||||||
// Verify any pending `not:` directives now that we know their range.
|
// Verify any pending `not:` directives now that we know their range.
|
||||||
for (not_begin, rx) in nots.drain(..) {
|
for (not_idx, not_begin, rx) in nots.drain(..) {
|
||||||
if let Some(_) = rx.find(&text[not_begin..match_begin]) {
|
state.recorder.directive(not_idx);
|
||||||
|
if let Some((s, e)) = rx.find(&text[not_begin..match_begin]) {
|
||||||
// Matched `not:` pattern.
|
// 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);
|
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.
|
// 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..]) {
|
if let Some(_) = rx.find(&text[not_begin..]) {
|
||||||
// Matched `not:` pattern.
|
// Matched `not:` pattern.
|
||||||
// TODO: Use matched range for an error message.
|
// TODO: Use matched range for an error message.
|
||||||
@@ -224,8 +242,10 @@ pub struct VarDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct State<'a> {
|
struct State<'a> {
|
||||||
env_vars: &'a VariableMap,
|
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
|
env_vars: &'a VariableMap,
|
||||||
|
recorder: &'a mut Recorder,
|
||||||
|
|
||||||
vars: HashMap<String, VarDef>,
|
vars: HashMap<String, VarDef>,
|
||||||
// Offset after the last ordered match. This does not include recent unordered matches.
|
// Offset after the last ordered match. This does not include recent unordered matches.
|
||||||
last_ordered: usize,
|
last_ordered: usize,
|
||||||
@@ -234,10 +254,11 @@ struct State<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> 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 {
|
State {
|
||||||
text: text,
|
text: text,
|
||||||
env_vars: env_vars,
|
env_vars: env_vars,
|
||||||
|
recorder: recorder,
|
||||||
vars: HashMap::new(),
|
vars: HashMap::new(),
|
||||||
last_ordered: 0,
|
last_ordered: 0,
|
||||||
max_match: 0,
|
max_match: 0,
|
||||||
@@ -309,8 +330,10 @@ impl<'a> State<'a> {
|
|||||||
rx.captures(txt).map(|caps| {
|
rx.captures(txt).map(|caps| {
|
||||||
let matched_range = caps.pos(0).expect("whole expression must match");
|
let matched_range = caps.pos(0).expect("whole expression must match");
|
||||||
for var in defs {
|
for var in defs {
|
||||||
|
let txtval = caps.name(var).unwrap_or("");
|
||||||
|
self.recorder.defined_var(var, txtval);
|
||||||
let vardef = VarDef {
|
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
|
// This offset is the end of the whole matched pattern, not just the text
|
||||||
// defining the variable.
|
// defining the variable.
|
||||||
offset: range.0 + matched_range.1,
|
offset: range.0 + matched_range.1,
|
||||||
@@ -320,7 +343,14 @@ impl<'a> State<'a> {
|
|||||||
matched_range
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
196
cranelift/src/libfilecheck/explain.rs
Normal file
196
cranelift/src/libfilecheck/explain.rs
Normal file
@@ -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<Match>,
|
||||||
|
vardefs: Vec<VarDef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,7 @@ mod error;
|
|||||||
mod variable;
|
mod variable;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod checker;
|
mod checker;
|
||||||
|
mod explain;
|
||||||
|
|
||||||
/// The range of a match in the input text.
|
/// The range of a match in the input text.
|
||||||
pub type MatchRange = (usize, usize);
|
pub type MatchRange = (usize, usize);
|
||||||
|
|||||||
@@ -20,10 +20,21 @@ pub fn run(files: Vec<String>, verbose: bool) -> CommandResult {
|
|||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
try!(io::stdin().read_to_string(&mut buffer).map_err(|e| format!("stdin: {}", e)));
|
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(())
|
Ok(())
|
||||||
} else {
|
} 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())
|
Err("Check failed".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
function nonsense(i32, i32) -> f32 {
|
function nonsense(i32, i32) -> f32 {
|
||||||
; check: digraph nonsense {
|
; check: digraph nonsense {
|
||||||
|
; regex: I=\binst\d+\b
|
||||||
|
; check: label="{ebb0 | <$(BRZ=$I)>brz ebb2 | <$(JUMP=$I)>jump ebb1}"]
|
||||||
|
|
||||||
ebb0(v1: i32, v2: i32):
|
ebb0(v1: i32, v2: i32):
|
||||||
v3 = f64const 0x0.0
|
v3 = f64const 0x0.0
|
||||||
brz v2, ebb2 ; unordered: ebb0:inst1 -> ebb2
|
brz v2, ebb2 ; unordered: ebb0:$BRZ -> ebb2
|
||||||
v4 = iconst.i32 0
|
v4 = iconst.i32 0
|
||||||
jump ebb1(v4) ; unordered: ebb0:inst3 -> ebb1
|
jump ebb1(v4) ; unordered: ebb0:$JUMP -> ebb1
|
||||||
|
|
||||||
ebb1(v5: i32):
|
ebb1(v5: i32):
|
||||||
v6 = imul_imm v5, 4
|
v6 = imul_imm v5, 4
|
||||||
|
|||||||
Reference in New Issue
Block a user