use crate::ir::{IrNode, Shape};
pub const BOILERPLATE_VERSION: &str = "boilerplate-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Boilerplate {
TrivialBody,
Forwarding,
MacroRepetition,
GuardedDispatch,
ConfiguredAnswer,
}
impl Boilerplate {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::TrivialBody => "trivial-body",
Self::Forwarding => "forwarding",
Self::MacroRepetition => "macro-repetition",
Self::GuardedDispatch => "guarded-dispatch",
Self::ConfiguredAnswer => "configured-answer",
}
}
#[must_use]
pub const fn all() -> [Self; 5] {
[
Self::TrivialBody,
Self::Forwarding,
Self::MacroRepetition,
Self::GuardedDispatch,
Self::ConfiguredAnswer,
]
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::all().into_iter().find(|kind| kind.name() == name)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BoilerplateCounts {
pub control: usize,
pub calls: usize,
pub macros: usize,
pub statements: usize,
pub work: usize,
pub branches: usize,
pub declarations: usize,
pub returns: usize,
}
#[must_use]
pub fn classify(unit: &IrNode) -> Option<Boilerplate> {
let body = counts(unit);
if body.control > 0 {
return dispatch(&body);
}
if body.macros >= 2 && body.calls == 0 && body.statements == 0 {
return Some(Boilerplate::MacroRepetition);
}
if body.macros > 0 {
return None;
}
if configured(&body) {
return Some(Boilerplate::ConfiguredAnswer);
}
match body.calls {
0 if body.statements <= 1 => Some(Boilerplate::TrivialBody),
1 if body.work == 0 => Some(Boilerplate::Forwarding),
_ => None,
}
}
const fn configured(body: &BoilerplateCounts) -> bool {
body.returns >= 2 && body.work == 0 && body.declarations == 0 && body.calls <= body.returns
}
fn dispatch(body: &BoilerplateCounts) -> Option<Boilerplate> {
let shaped = body.branches == 1
&& body.control == body.branches
&& body.macros == 0
&& body.work == 0
&& body.declarations == 0
&& body.returns >= 2;
(shaped && body.calls <= body.returns).then_some(Boilerplate::GuardedDispatch)
}
#[must_use]
pub fn counts(unit: &IrNode) -> BoilerplateCounts {
let mut body = BoilerplateCounts::default();
for child in &unit.children {
descend(child, false, &mut body);
}
body
}
fn descend(node: &IrNode, in_call: bool, body: &mut BoilerplateCounts) {
tally(node, in_call, body);
let nested = in_call || node.shape == Shape::Call;
for child in &node.children {
descend(child, nested, body);
}
}
fn tally(node: &IrNode, in_call: bool, body: &mut BoilerplateCounts) {
match node.shape {
Shape::Branch => {
body.control += 1;
body.branches += 1;
}
Shape::Loop | Shape::Match | Shape::MatchArm | Shape::Break | Shape::Continue => {
body.control += 1;
}
Shape::Try => {
if handles(node) {
body.control += 1;
}
}
Shape::Call => {
if !in_call {
body.calls += 1;
}
}
Shape::MacroCall => {
if !in_call {
body.macros += 1;
}
}
Shape::Return => {
body.statements += 1;
body.returns += 1;
}
Shape::VarDecl => {
body.statements += 1;
body.declarations += 1;
}
Shape::Assign | Shape::ExprStmt => {
body.statements += 1;
body.work += 1;
}
_ => {}
}
}
fn handles(node: &IrNode) -> bool {
node.children
.iter()
.any(|child| child.shape == Shape::Block)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::ir::ByteRange;
fn unit(shapes: &[Shape]) -> IrNode {
let body = IrNode {
shape: Shape::Block,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: shapes
.iter()
.cloned()
.map(|shape| IrNode {
shape,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: Vec::new(),
})
.collect(),
};
IrNode {
shape: Shape::Function,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: vec![body],
}
}
#[test]
fn a_body_that_moves_one_value_is_trivial() {
assert_eq!(classify(&unit(&[])), Some(Boilerplate::TrivialBody));
assert_eq!(
classify(&unit(&[Shape::Assign])),
Some(Boilerplate::TrivialBody)
);
assert_eq!(
classify(&unit(&[Shape::Return])),
Some(Boilerplate::TrivialBody)
);
assert_eq!(classify(&unit(&[Shape::Assign, Shape::Return])), None);
}
#[test]
fn exported_counts_are_the_classifier_input() {
let measured = counts(&unit(&[Shape::Assign, Shape::Return]));
assert_eq!(measured.control, 0);
assert_eq!(measured.calls, 0);
assert_eq!(measured.macros, 0);
assert_eq!(measured.statements, 2);
assert_eq!(measured.work, 1);
assert_eq!(measured.branches, 0);
assert_eq!(measured.declarations, 0);
assert_eq!(measured.returns, 1);
}
#[test]
fn a_single_call_and_nothing_else_is_forwarding() {
assert_eq!(
classify(&unit(&[Shape::Call])),
Some(Boilerplate::Forwarding)
);
assert_eq!(classify(&unit(&[Shape::Call, Shape::Assign])), None);
assert_eq!(classify(&unit(&[Shape::Call, Shape::Call])), None);
}
#[test]
fn a_local_the_delegation_answers_through_is_part_of_the_call() {
assert_eq!(
classify(&unit(&[Shape::VarDecl, Shape::Call, Shape::Return])),
Some(Boilerplate::Forwarding)
);
assert_eq!(
classify(&unit(&[
Shape::VarDecl,
Shape::VarDecl,
Shape::Call,
Shape::Return
])),
Some(Boilerplate::Forwarding)
);
assert_eq!(classify(&unit(&[Shape::VarDecl, Shape::Return])), None);
}
#[test]
fn handing_an_error_upwards_is_not_a_second_path() {
let propagate = nest(Shape::Call, vec![nest(Shape::Try, vec![leaf(Shape::Call)])]);
assert_eq!(
classify(&unit_of(vec![propagate])),
Some(Boilerplate::Forwarding)
);
let handle = nest(
Shape::Try,
vec![
nest(Shape::Block, vec![leaf(Shape::Call)]),
nest(Shape::Block, vec![leaf(Shape::Call)]),
],
);
assert_eq!(classify(&unit_of(vec![handle])), None);
}
fn nest(shape: Shape, children: Vec<IrNode>) -> IrNode {
IrNode {
shape,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children,
}
}
fn leaf(shape: Shape) -> IrNode {
nest(shape, Vec::new())
}
fn unit_of(statements: Vec<IrNode>) -> IrNode {
nest(Shape::Function, vec![nest(Shape::Block, statements)])
}
#[test]
fn the_arguments_of_a_delegation_are_part_of_it() {
let delegation = nest(Shape::Call, vec![leaf(Shape::Call)]);
assert_eq!(
classify(&unit_of(vec![delegation])),
Some(Boilerplate::Forwarding)
);
let wrapped = nest(
Shape::Return,
vec![nest(
Shape::Call,
vec![leaf(Shape::Call), leaf(Shape::Call)],
)],
);
assert_eq!(
classify(&unit_of(vec![wrapped])),
Some(Boilerplate::Forwarding)
);
}
#[test]
fn a_macro_inside_a_delegation_is_part_of_it() {
let delegation = nest(
Shape::Call,
vec![nest(Shape::Call, vec![leaf(Shape::MacroCall)])],
);
assert_eq!(
classify(&unit_of(vec![delegation])),
Some(Boilerplate::Forwarding)
);
let body = vec![leaf(Shape::Call), leaf(Shape::MacroCall)];
assert_eq!(classify(&unit_of(body)), None);
}
#[test]
fn a_repetition_of_macros_cannot_hide_inside_a_call() {
let body = vec![
nest(Shape::Call, vec![leaf(Shape::MacroCall)]),
nest(Shape::Call, vec![leaf(Shape::MacroCall)]),
];
assert_eq!(classify(&unit_of(body)), None);
}
#[test]
fn two_calls_side_by_side_are_two_things_done() {
let body = vec![
nest(Shape::Call, vec![leaf(Shape::Call)]),
leaf(Shape::Call),
];
assert_eq!(classify(&unit_of(body)), None);
}
#[test]
fn work_beside_a_delegation_still_disqualifies_it() {
let body = vec![
nest(Shape::Call, vec![leaf(Shape::Call)]),
leaf(Shape::Assign),
];
assert_eq!(classify(&unit_of(body)), None);
}
#[test]
fn a_run_of_macro_invocations_is_recognised_as_repetition() {
assert_eq!(
classify(&unit(&[
Shape::MacroCall,
Shape::MacroCall,
Shape::MacroCall
])),
Some(Boilerplate::MacroRepetition)
);
assert_eq!(classify(&unit(&[Shape::MacroCall])), None);
assert_eq!(
classify(&unit(&[Shape::MacroCall, Shape::MacroCall, Shape::Return])),
None
);
}
#[test]
fn a_guard_and_an_answer_on_each_side_is_a_dispatch() {
let guarded = vec![
nest(Shape::Branch, vec![leaf(Shape::Return)]),
leaf(Shape::Return),
];
assert_eq!(
classify(&unit_of(guarded)),
Some(Boilerplate::GuardedDispatch)
);
let dispatched = vec![
nest(
Shape::Branch,
vec![nest(Shape::Return, vec![leaf(Shape::Call)])],
),
nest(Shape::Return, vec![leaf(Shape::Call)]),
];
assert_eq!(
classify(&unit_of(dispatched)),
Some(Boilerplate::GuardedDispatch)
);
}
#[test]
fn two_answers_and_no_guard_are_the_build_configuration_choosing() {
let configured = vec![
nest(Shape::Return, vec![leaf(Shape::Call)]),
nest(Shape::Return, vec![leaf(Shape::Call)]),
];
assert_eq!(
classify(&unit_of(configured)),
Some(Boilerplate::ConfiguredAnswer)
);
let three = vec![
leaf(Shape::Return),
leaf(Shape::Return),
leaf(Shape::Return),
];
assert_eq!(
classify(&unit_of(three)),
Some(Boilerplate::ConfiguredAnswer)
);
}
#[test]
fn arms_that_do_something_are_written_once_per_configuration() {
let declaring = vec![
leaf(Shape::VarDecl),
nest(Shape::Return, vec![leaf(Shape::Call)]),
nest(Shape::Return, vec![leaf(Shape::Call)]),
];
assert_eq!(classify(&unit_of(declaring)), None);
let assigning = vec![
leaf(Shape::Assign),
leaf(Shape::Return),
leaf(Shape::Return),
];
assert_eq!(classify(&unit_of(assigning)), None);
let computing = vec![
nest(Shape::Return, vec![leaf(Shape::Call)]),
nest(
Shape::Return,
vec![leaf(Shape::Call), leaf(Shape::Call), leaf(Shape::Call)],
),
];
assert_eq!(classify(&unit_of(computing)), None);
}
#[test]
fn one_answer_is_not_a_configuration() {
let single = vec![nest(Shape::Return, vec![leaf(Shape::Call)])];
assert_eq!(classify(&unit_of(single)), Some(Boilerplate::Forwarding));
}
#[test]
fn more_than_one_guard_is_a_decision_table() {
let table = vec![
nest(Shape::Branch, vec![leaf(Shape::Return)]),
nest(Shape::Branch, vec![leaf(Shape::Return)]),
leaf(Shape::Return),
];
assert_eq!(classify(&unit_of(table)), None);
}
#[test]
fn work_beside_a_guard_is_not_a_choice_between_answers() {
let assigning = vec![
nest(Shape::Branch, vec![leaf(Shape::Assign)]),
leaf(Shape::Return),
];
assert_eq!(classify(&unit_of(assigning)), None);
let declaring = vec![
leaf(Shape::VarDecl),
nest(Shape::Branch, vec![leaf(Shape::Return)]),
leaf(Shape::Return),
];
assert_eq!(classify(&unit_of(declaring)), None);
let computing = vec![
nest(Shape::Branch, vec![leaf(Shape::Return)]),
nest(
Shape::Return,
vec![leaf(Shape::Call), leaf(Shape::Call), leaf(Shape::Call)],
),
];
assert_eq!(classify(&unit_of(computing)), None);
}
#[test]
fn control_flow_other_than_one_guard_is_never_boilerplate() {
for shape in [Shape::Branch, Shape::Loop, Shape::Match] {
assert_eq!(
classify(&unit(std::slice::from_ref(&shape))),
None,
"{shape:?}"
);
assert_eq!(classify(&unit(&[Shape::Call, shape])), None);
}
}
#[test]
fn nested_bodies_count_towards_the_unit() {
let mut node = unit(&[]);
node.children[0].children.push(IrNode {
shape: Shape::Closure,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: vec![IrNode {
shape: Shape::Branch,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: Vec::new(),
}],
});
assert_eq!(classify(&node), None);
}
#[test]
fn category_names_round_trip() {
for category in Boilerplate::all() {
assert_eq!(Boilerplate::from_name(category.name()), Some(category));
}
assert_eq!(Boilerplate::from_name("getter"), None);
}
}