Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add transition coverage instrumentation #25

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions expandable-impl/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::env;

const EXPANDABLE_INSTRUMENT: &str = "EXPANDABLE_INSTRUMENT";

fn main() {
println!("cargo:rerun-if-env-changed={EXPANDABLE_INSTRUMENT}");

if env::var(EXPANDABLE_INSTRUMENT).is_ok() {
println!("cargo:rustc-cfg=transition_coverage");
}
}
17 changes: 13 additions & 4 deletions expandable-impl/src/grammar.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Architectural invariant: this module contains basic types that allow to parse
// the Rust language.

#[cfg(transition_coverage)]
mod log;

use smallvec::{smallvec, SmallVec};

use crate::{FragmentKind, Terminal};
Expand All @@ -23,9 +26,12 @@ impl DynamicState {
self,
descr: TokenDescription,
) -> Result<DynamicState, Vec<TokenDescription>> {
self.state
.trans(descr, self.stack_top())
.map(|transition| self.with(transition))
self.state.trans(descr, self.stack_top()).map(|transition| {
#[cfg(transition_coverage)]
log::log_transition(self.state, descr, self.stack_top(), transition);

self.with(transition)
})
}

pub(crate) fn is_accepting(&self) -> bool {
Expand Down Expand Up @@ -69,7 +75,7 @@ impl DynamicState {
}
}

#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Transition {
pub(crate) state: State,
pub(crate) pop: bool,
Expand Down Expand Up @@ -184,10 +190,12 @@ macro_rules! generate_grammar {
}

impl $name {
#[allow(unused_variables)]
const TRANSITIONS: &'static[
(
&'static [(TokenDescription, Option<StackSymbol>, State, Option<StackSymbol>)],
Option<State>,
$name,
)
] = &[
$(
Expand All @@ -203,6 +211,7 @@ macro_rules! generate_grammar {
),*
],
generate_grammar!(@inherit $( $inherit )?),
generate_grammar!(@state $in_state),
)
),*
];
Expand Down
156 changes: 156 additions & 0 deletions expandable-impl/src/grammar/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use std::{
fmt::{Debug, Write},
fs,
fs::OpenOptions,
io::Write as _,
path::Path,
process,
sync::Mutex,
};

use crate::{
grammar::{StackSymbol, State, Transition},
TokenDescription,
};

pub(crate) fn log_transition(
state: State,
descr: TokenDescription,
top: Option<StackSymbol>,
transition: Transition,
) {
if !Path::new("target/coverage/all.json").exists() {
// Let's pretend fs errors never happen for a while.
log_all_transitions();
}
let dir_path = format!("target/coverage/{:?}", process::id());
fs::create_dir_all(&dir_path).expect("failed to create coverage directory");

let file_path = format!(
"target/coverage/{:?}/{:?}.json",
process::id(),
transition_id()
);
let transition = LoggableTransition::from_actual_transition(state, descr, top, transition);

let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)
.expect("failed to create coverage file");

writeln!(file, "{}", transition.pp()).expect("failed to write to coverage file");
}

struct LoggableTransition {
in_state: State,
in_top: Option<StackSymbol>,
descr: TokenDescription,
out_state: State,
out_top: Option<StackSymbol>,
}

impl LoggableTransition {
fn from_actual_transition(
state: State,
descr: TokenDescription,
top: Option<StackSymbol>,
transition: Transition,
) -> LoggableTransition {
// Don't count the top symbol as consumed if it has not been popped.
let top = if transition.pop { top } else { None };

LoggableTransition {
in_state: state,
in_top: top,
descr,
out_state: transition.state,
out_top: transition.push,
}
}

fn pp(&self) -> String {
let LoggableTransition {
in_state,
in_top,
descr,
out_state,
out_top,
} = self;

let mut fields = vec![
("in_state", in_state as &dyn Debug),
("out_state", out_state as &dyn Debug),
("token", &descr as &dyn Debug),
];

if let Some(in_top) = in_top {
fields.push(("in_top", in_top as &dyn Debug));
}

if let Some(out_top) = out_top {
fields.push(("out_top", out_top as &dyn Debug));
}

let mut buf = String::new();

buf.push_str("{");

for (name, value) in fields {
write!(buf, " \"{name}\": \"{:?}\",", value).expect("failed to write to string");
}

buf.pop();
buf.push_str(" }");

buf
}
}

fn transition_id() -> usize {
static TRANSITION_ID: Mutex<usize> = Mutex::new(0);

let mut lock = TRANSITION_ID.lock().unwrap();
let id = *lock;
*lock += 1;
id
}

fn log_all_transitions() {
fs::create_dir_all("target/coverage").expect("failed to create coverage directory");
let mut file = OpenOptions::new()
.create_new(true)
.append(true)
.open("target/coverage/all.json")
.expect("failed to create coverage file");

writeln!(file, "[").expect("failed to write to coverage file");

let mut first = true;
State::TRANSITIONS
.iter()
.flat_map(|(transes, _, state)| {
transes
.iter()
.map(|(descr, pop, out, push)| (*state, descr, pop, out, push))
})
.for_each(|(state, descr, pop, out, push)| {
if first {
first = false;
} else {
writeln!(file, ",").expect("failed to write to coverage file");
}

write!(
file,
" {{ \"in_state\": \"{:?}\", \"out_state\": \"{:?}\", \"token\": \"{:?}\", \
\"in_top\": \"{:?}\", \"out_top\": \"{:?}\" }}",
state, out, descr, pop, push
)
.expect("failed to write to coverage file");
});

writeln!(file, ",\n]");
}

fn log_transitions(f: &mut impl Write, state: State) {}