use std::fmt::Write as _;
use crate::ast::{Rule, Term, TermKind};
use crate::error::Error;
use crate::matcher::{Matcher, Test};
const HELPERS: &[(&str, &str)] = &[
("sign_extend", SIGN_EXTEND),
("zero_extend", ZERO_EXTEND),
("extract", EXTRACT),
("shifted", SHIFTED),
("low", LOW),
];
pub fn emit(source: &str, rules: &[Rule], matcher: &Matcher) -> Result<String, Vec<Error>> {
let mut out = String::new();
let mut errors = Vec::new();
let mut wanted: Vec<&'static str> = Vec::new();
let guards = compile_guards(source, rules, &mut wanted, &mut errors);
if !errors.is_empty() {
return Err(errors);
}
header(&mut out, source, rules, matcher);
nodes(&mut out, matcher);
lowerings(&mut out, source, rules, &guards);
out.push_str(&guards.iter().flatten().map(String::as_str).collect::<String>());
helpers(&mut out, &wanted);
Ok(out)
}
fn header(out: &mut String, source: &str, rules: &[Rule], matcher: &Matcher) {
let _ = write!(
out,
"\
// Generated from {source} by rucc-rules. Do not edit this file: edit the
// rule file and build again. It holds {} rules over {} trie nodes.
//
// The types are the ones the module that includes this file defines, and the walk over the
// table is there too. What is here is the table.
use super::{{Node, Piece, Rule, Table, Test}};
/// The rule file this table was built from, so that anything said about a rule can name a file
/// somebody can open.
pub const SOURCE: &str = {source:?};
/// The lowering rules of this target, as an automaton over their patterns.
pub static TABLE: Table = Table {{ source: SOURCE, nodes: NODES, rules: LOWERINGS }};
",
rules.len(),
matcher.nodes.len()
);
}
fn nodes(out: &mut String, matcher: &Matcher) {
out.push_str(
"\n/// The trie over the patterns. A node holds the tests to try in order, the branch\n\
/// that takes anything, and the rule that ends here if one does.\nstatic NODES: \
&[Node] = &[\n",
);
for (index, node) in matcher.nodes.iter().enumerate() {
let _ = writeln!(out, " // {index}");
out.push_str(" Node {\n tests: &[");
for (test, next) in &node.tests {
match test {
Test::App { head, arity } => {
let _ = write!(
out,
"\n (Test::App {{ head: {head:?}, arity: {arity} }}, {next}),"
);
}
Test::Int(value) => {
let _ = write!(out, "\n (Test::Int({value}), {next}),");
}
}
}
if !node.tests.is_empty() {
out.push_str("\n ");
}
out.push_str("],\n");
match &node.wildcard {
Some((name, next)) => {
let _ = writeln!(out, " wildcard: Some(({name:?}, {next})),");
}
None => out.push_str(" wildcard: None,\n"),
}
match node.accept {
Some(rule) => {
let _ = writeln!(out, " accept: Some({rule}),");
}
None => out.push_str(" accept: None,\n"),
}
out.push_str(" },\n");
}
out.push_str("];\n");
}
fn lowerings(out: &mut String, source: &str, rules: &[Rule], guards: &[Option<String>]) {
out.push_str(
"\n/// The rules, in the order the rule file writes them, which is the order the\n\
/// `accept` of a trie node names.\nstatic LOWERINGS: &[Rule] = &[\n",
);
for (index, rule) in rules.iter().enumerate() {
let pattern = rule.pattern.to_string();
let _ = writeln!(out, " // {source}:{}", rule.line);
out.push_str(" Rule {\n");
let _ = writeln!(out, " pattern: {pattern:?},");
out.push_str(" replacement: &[");
let bound = bound_names(&rule.pattern);
for piece in pieces(&rule.replacement, &bound) {
let _ = write!(out, "\n {piece},");
}
out.push_str("\n ],\n");
match guards[index] {
Some(_) => {
let _ = writeln!(out, " guard: Some(guard_{index}),");
}
None => out.push_str(" guard: None,\n"),
}
let _ = writeln!(out, " line: {},", rule.line);
out.push_str(" },\n");
}
out.push_str("];\n");
}
fn bound_names(pattern: &Term) -> Vec<String> {
let mut out = Vec::new();
pattern.walk(&mut |term| {
if let TermKind::Var(name) = &term.kind {
out.push(name.clone());
}
});
out
}
fn pieces(term: &Term, bound: &[String]) -> Vec<String> {
let mut out = Vec::new();
push_pieces(term, bound, &mut out);
out
}
fn push_pieces(term: &Term, bound: &[String], out: &mut Vec<String>) {
match &term.kind {
TermKind::Var(name) => {
let index = bound.iter().position(|have| have == name).unwrap_or_default();
out.push(format!("Piece::Var {{ name: {name:?}, index: {index} }}"));
}
TermKind::Int(value) => out.push(format!("Piece::Int({value})")),
TermKind::App { head, args } => {
out.push(format!("Piece::App {{ head: {head:?}, arity: {} }}", args.len()));
for arg in args {
push_pieces(arg, bound, out);
}
}
}
}
fn compile_guards(
source: &str,
rules: &[Rule],
wanted: &mut Vec<&'static str>,
errors: &mut Vec<Error>,
) -> Vec<Option<String>> {
let mut out = Vec::with_capacity(rules.len());
for (index, rule) in rules.iter().enumerate() {
let Some(guard) = &rule.guard else {
out.push(None);
continue;
};
let bound = bound_names(&rule.pattern);
let mut used = Vec::new();
let condition = match condition(source, guard, &bound, wanted, &mut used) {
Ok(text) => text,
Err(error) => {
errors.push(error);
out.push(None);
continue;
}
};
let mut text = format!(
"\n/// `{guard}`, which is the guard of the rule on line {}.\nfn guard_{index}(bound: \
&[Option<i128>]) -> bool {{\n",
rule.line
);
used.sort_unstable();
used.dedup();
for at in used {
let _ = writeln!(
text,
" // {}\n let Some(Some(v{at})) = bound.get({at}).copied() else {{ return \
false }};",
bound[at]
);
}
let _ = writeln!(text, " {}\n}}", bare(&condition));
out.push(Some(text));
}
out
}
fn bare(text: &str) -> &str {
let Some(inner) = text.strip_prefix('(').and_then(|text| text.strip_suffix(')')) else {
return text;
};
let mut depth = 0i32;
for c in inner.chars() {
match c {
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
if depth < 0 {
return text;
}
}
inner
}
fn condition(
source: &str,
term: &Term,
bound: &[String],
wanted: &mut Vec<&'static str>,
used: &mut Vec<usize>,
) -> Result<String, Error> {
let TermKind::App { head, args } = &term.kind else {
return Err(refused(source, term, "a guard is a condition, and this is not one"));
};
let arity = args.len();
match (head.as_str(), arity) {
("and" | "or", 1..) => {
let joint = if head == "and" { " && " } else { " || " };
let mut parts = Vec::with_capacity(arity);
for arg in args {
parts.push(condition(source, arg, bound, wanted, used)?);
}
Ok(format!("({})", parts.join(joint)))
}
("not", 1) => Ok(format!("!{}", condition(source, &args[0], bound, wanted, used)?)),
("=" | "!=" | "<" | "<=" | ">" | ">=", 2) => {
let operator = if head == "=" { "==" } else { head.as_str() };
let left = value(source, &args[0], bound, wanted, used)?;
let right = value(source, &args[1], bound, wanted, used)?;
Ok(format!("({left} {operator} {right})"))
}
_ => Err(refused(
source,
term,
&format!(
"`{head}` of {arity} is not a condition a guard can be compiled to. A guard is \
`and`, `or`, `not`, or a comparison of two numbers"
),
)),
}
}
fn value(
source: &str,
term: &Term,
bound: &[String],
wanted: &mut Vec<&'static str>,
used: &mut Vec<usize>,
) -> Result<String, Error> {
match &term.kind {
TermKind::Int(number) => Ok(format!("{number}")),
TermKind::Var(name) => {
let at = bound.iter().position(|have| have == name).unwrap_or_default();
used.push(at);
Ok(format!("v{at}"))
}
TermKind::App { head, args } => {
let arity = args.len();
match (head.as_str(), arity) {
("sign_extend" | "zero_extend" | "extract", 3) => {
let first = width(source, &args[0])?;
let second = width(source, &args[1])?;
let inner = value(source, &args[2], bound, wanted, used)?;
let name = match head.as_str() {
"sign_extend" => "sign_extend",
"zero_extend" => "zero_extend",
_ => "extract",
};
want(wanted, name);
Ok(format!("{name}({first}, {second}, {inner})"))
}
_ => Err(refused(
source,
term,
&format!(
"`{head}` of {arity} is not a number a guard can be compiled to. The \
ones that are are `sign_extend`, `zero_extend` and `extract`"
),
)),
}
}
}
}
fn width(source: &str, term: &Term) -> Result<String, Error> {
match &term.kind {
TermKind::Int(number) if (0..=128).contains(number) => Ok(format!("{number}")),
_ => Err(refused(source, term, "a width has to be a number from 0 to 128")),
}
}
fn want(wanted: &mut Vec<&'static str>, name: &'static str) {
if wanted.contains(&name) {
return;
}
wanted.push(name);
match name {
"sign_extend" => want(wanted, "shifted"),
"zero_extend" | "extract" => want(wanted, "low"),
_ => {}
}
}
fn helpers(out: &mut String, wanted: &[&str]) {
for (name, text) in HELPERS {
if wanted.contains(name) {
out.push_str(text);
}
}
}
fn refused(source: &str, term: &Term, message: &str) -> Error {
Error {
path: source.to_owned(),
line: term.line,
column: term.column,
message: message.to_owned(),
}
}
const SIGN_EXTEND: &str = "
/// The low `from` bits of `value`, sign extended to `to` bits.
fn sign_extend(from: u32, to: u32, value: i128) -> i128 {
shifted(to, shifted(from, value))
}
";
const ZERO_EXTEND: &str = "
/// The low `from` bits of `value`, read as a number and not sign extended.
fn zero_extend(from: u32, to: u32, value: i128) -> i128 {
low(to, low(from, value))
}
";
const EXTRACT: &str = "
/// The bits from `hi` down to `lo` of `value`, read as a number.
fn extract(hi: u32, lo: u32, value: i128) -> i128 {
if lo >= 128 || hi < lo {
return 0;
}
low(hi - lo + 1, value >> lo)
}
";
const SHIFTED: &str = "
/// `value` read as a signed number that many bits wide.
fn shifted(bits: u32, value: i128) -> i128 {
match 128u32.checked_sub(bits) {
Some(room) if room > 0 => (value << room) >> room,
_ => value,
}
}
";
const LOW: &str = "
/// The low `bits` bits of `value`, read as a number.
fn low(bits: u32, value: i128) -> i128 {
if bits >= 128 {
return value;
}
#[allow(clippy::cast_possible_wrap)]
let masked = (value as u128 & ((1u128 << bits) - 1)) as i128;
masked
}
";
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
fn built(text: &str) -> String {
let rules = parse("rules/test.rules", text).expect("the rules read");
let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
emit("rules/test.rules", &rules, &matcher).expect("the table is emitted")
}
#[test]
fn a_rule_set_comes_out_as_a_table_of_nodes_and_a_table_of_rules() {
let out = built(
"(rule (lower (add.i64 (value.i64 x) (value.i64 y)))\n\
(x64.add_rr_64 x y)\n\
(spec (= (bvadd x y) (result))))\n",
);
assert!(out.contains("use super::{Node, Piece, Rule, Table, Test};"), "{out}");
assert!(out.contains("pub const SOURCE: &str = \"rules/test.rules\";"), "{out}");
assert!(out.contains("(Test::App { head: \"add.i64\", arity: 2 }, 1),"), "{out}");
assert!(out.contains("wildcard: Some((\"x\", 3)),"), "{out}");
assert!(out.contains("accept: Some(0),"), "{out}");
assert!(out.contains("Piece::App { head: \"x64.add_rr_64\", arity: 2 }"), "{out}");
assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
assert!(out.contains("Piece::Var { name: \"y\", index: 1 }"), "{out}");
assert!(out.contains("guard: None,"), "{out}");
}
#[test]
fn a_guard_comes_out_as_a_function_of_the_bindings() {
let out = built(
"(rule (lower (shl.i64 (value.i64 x) (iconst.i64 k)))\n\
(if (and (>= k 0) (< k 64)))\n\
(x64.shl_ri_64 x k)\n\
(spec (= (bvshl x k) (result))))\n",
);
assert!(out.contains("guard: Some(guard_0),"), "{out}");
assert!(out.contains("fn guard_0(bound: &[Option<i128>]) -> bool {"), "{out}");
assert!(
out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
"{out}"
);
assert!(out.contains("(v1 >= 0) && (v1 < 64)"), "{out}");
assert!(!out.contains("fn sign_extend"), "{out}");
assert!(!out.contains("fn low"), "{out}");
}
#[test]
fn a_guard_that_reads_bits_brings_the_helpers_it_needs() {
let out = built(
"(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
(if (= k (sign_extend 32 64 (extract 31 0 k))))\n\
(x64.add_ri_64 x k)\n\
(spec (= (bvadd x k) (result))))\n",
);
assert!(out.contains("v1 == sign_extend(32, 64, extract(31, 0, v1))"), "{out}");
assert!(out.contains("fn sign_extend(from: u32, to: u32, value: i128) -> i128 {"), "{out}");
assert!(out.contains("fn shifted(bits: u32, value: i128) -> i128 {"), "{out}");
assert!(out.contains("fn extract(hi: u32, lo: u32, value: i128) -> i128 {"), "{out}");
assert!(out.contains("fn low(bits: u32, value: i128) -> i128 {"), "{out}");
assert!(!out.contains("fn zero_extend"), "{out}");
}
#[test]
fn a_guard_nothing_can_be_made_of_is_refused_where_it_is_written() {
let rules = parse(
"rules/test.rules",
"(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
(if (fits_in_a_byte k))\n\
(x64.add_ri_64 x k)\n\
(spec (= (bvadd x k) (result))))\n",
)
.expect("the rules read");
let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
let errors = emit("rules/test.rules", &rules, &matcher).expect_err("the guard is refused");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 2);
assert!(
errors[0].message.contains("`fits_in_a_byte` of 1 is not a condition"),
"{}",
errors[0]
);
}
}