use rucc_rules::{Matcher, Rule, Term, parse};
const RULES: &str = "\
(rule (lower (add.i64 (value x) (mul.i64 (value y) (iconst 4))))
(x64.lea (amode_base_index_scale x y 4))
(spec (= (bvadd x (bvmul y 4)) (result))))
(rule (lower (add.i64 (value x) (shl.i64 (value y) (iconst k))))
(if (and (>= k 0) (< k 64)))
(a64.add_shifted x y (lsl k))
(spec (= (bvadd x (bvshl y k)) (result))))
(rule (lower (add.i64 (value x) (mul.i64 (value y) (iconst m))))
(x64.imul_then_add x y m)
(spec (= (bvadd x (bvmul y m)) (result))))
(rule (lower (add.i64 (value x) (value y)))
(x64.add x y)
(spec (= (bvadd x y) (result))))";
fn rules(text: &str) -> Vec<Rule> {
match parse("t.rules", text) {
Ok(rules) => rules,
Err(errors) => panic!("{}", errors[0]),
}
}
fn matcher(text: &str) -> Matcher {
match Matcher::build("t.rules", &rules(text)) {
Ok(matcher) => matcher,
Err(errors) => panic!("{}", errors[0]),
}
}
fn term(text: &str) -> Term {
let text = format!("(rule (lower {text}) (nothing) (spec (= (result) (result))))");
match parse("t.rules", &text) {
Ok(rules) => rules.into_iter().next().expect("one rule").pattern,
Err(errors) => panic!("{}", errors[0]),
}
}
#[test]
fn the_rule_that_names_the_most_is_the_rule_that_fires() {
let matcher = matcher(RULES);
let subject = term("(add.i64 (value a) (mul.i64 (value b) (iconst 4)))");
let found = matcher.find(&subject).expect("something has to match");
assert_eq!(found.rule, 0);
assert_eq!(found.get("x").map(ToString::to_string).as_deref(), Some("a"));
assert_eq!(found.get("y").map(ToString::to_string).as_deref(), Some("b"));
}
#[test]
fn a_literal_that_does_not_match_falls_through_to_the_more_general_rule() {
let matcher = matcher(RULES);
let subject = term("(add.i64 (value a) (mul.i64 (value b) (iconst 8)))");
let found = matcher.find(&subject).expect("the general rule has to catch it");
assert_eq!(found.rule, 2);
assert_eq!(found.get("m").map(ToString::to_string).as_deref(), Some("8"));
}
#[test]
fn a_shift_by_a_constant_reaches_the_rule_that_asked_for_one() {
let matcher = matcher(RULES);
let subject = term("(add.i64 (value a) (shl.i64 (value b) (iconst 3)))");
let found = matcher.find(&subject).expect("something has to match");
assert_eq!(found.rule, 1);
assert_eq!(found.get("k").map(ToString::to_string).as_deref(), Some("3"));
}
#[test]
fn nothing_matches_when_nothing_matches() {
let matcher = matcher(RULES);
assert!(matcher.find(&term("(sub.i64 (value a) (value b))")).is_none());
assert!(matcher.find(&term("(add.i64 (value a))")).is_none());
assert_eq!(matcher.find(&term("(add.i64 (value a) (value b))")).map(|m| m.rule), Some(3));
}
#[test]
fn rules_that_start_the_same_way_share_the_steps_they_agree_on() {
let shown = matcher(RULES).to_string();
assert_eq!(shown.matches("add.i64/2").count(), 1, "{shown}");
assert_eq!(shown.matches("value/1").count(), 4, "{shown}");
}
#[test]
fn the_wildcard_is_the_last_thing_tried_at_every_node() {
let shown = matcher(RULES).to_string();
let expected = "\
add.i64/2
value/1
bind x
mul.i64/2
value/1
bind y
iconst/1
4
=> rule 0
bind m
=> rule 2
shl.i64/2
value/1
bind y
iconst/1
bind k
=> rule 1
value/1
bind y
=> rule 3
";
assert_eq!(shown, expected);
}
#[test]
fn a_rule_that_can_never_fire_is_refused_rather_than_dropped() {
let text = "\
(rule (lower (add.i64 (value x) (value y))) (x64.add x y) (spec (= (bvadd x y) (result))))
(rule (lower (add.i64 (value a) (value b))) (x64.lea a b) (spec (= (bvadd a b) (result))))";
let Err(errors) = Matcher::build("t.rules", &rules(text)) else {
panic!("that was supposed to be refused");
};
assert_eq!(
errors[0].to_string(),
"t.rules:2:1: this rule can never fire, because the rule on line 1 matches everything it does"
);
}
#[test]
fn an_empty_rule_set_matches_nothing_and_says_so() {
let matcher = Matcher::build("t.rules", &[]).expect("nothing to refuse");
assert!(matcher.is_empty());
assert!(matcher.find(&term("(add.i64 (value a) (value b))")).is_none());
}