Skip to main content

rucc_rules/
emit.rs

1//! The matcher as Rust, for the compiler to link against.
2//!
3//! `spec/10-backend.md` section 10.2 asks for a generated automaton rather than a chain of
4//! conditionals, and this is the half of that which leaves this crate. A build script reads a
5//! rule file, builds the trie next door, and writes what this module produces into the build
6//! directory of the crate that matches with it. Nothing here is a copy of anything: the rule
7//! file is the only place the rules are written, and the table is regenerated whenever it
8//! changes.
9//!
10//! What comes out does not depend on whether the rules lower or simplify. The kind decides
11//! what a replacement is written in and therefore what the crate including the file does with
12//! it, and that crate already knows which file it asked for. A table of rewrite rules and a
13//! table of lowering rules are the same array of nodes and the same array of replacements.
14//!
15//! # What comes out
16//!
17//! One Rust source file, holding the trie as an array of nodes, the rules as an array of
18//! replacements, and one function per guard. It is data and not code, except for the guards,
19//! which are the one part of a rule that has to be evaluated rather than looked up. The walk
20//! over the table lives in the crate that includes the file, because the subject of a match
21//! there is the compiler's own IR rather than a term, and because a walk written once is a
22//! walk written once however many targets there are.
23//!
24//! The types the file names are the ones that crate defines, and it refers to them through
25//! `super`, which is what makes the file includable and nothing else. That is the whole of the
26//! contract between the two, and it is small on purpose.
27//!
28//! # Guards
29//!
30//! A guard is a condition on the constants a pattern matched, so it becomes a function of the
31//! values the bindings hold. A binding that is not a constant at all makes the guard false
32//! rather than an error, because a rule guarded by a claim about a number is a rule that does
33//! not fire when the operand is not one.
34//!
35//! The language a guard may be written in is small and this module is where it ends. A head it
36//! does not know is refused with the line it is on, rather than emitted and discovered as a
37//! compile error in generated code, which is the sort of message nobody can act on.
38
39use std::fmt::Write as _;
40
41use crate::ast::{Rule, Term, TermKind};
42use crate::error::Error;
43use crate::matcher::{Matcher, Test};
44
45/// The helpers a guard can call, and what each one needs emitted with it.
46const HELPERS: &[(&str, &str)] = &[
47    ("sign_extend", SIGN_EXTEND),
48    ("zero_extend", ZERO_EXTEND),
49    ("extract", EXTRACT),
50    ("shifted", SHIFTED),
51    ("low", LOW),
52];
53
54/// Turn a rule set and the trie it compiles into into Rust.
55///
56/// `source` is the rule file as it should be named in the generated file and in anything the
57/// compiler says about a rule at run time, so it is the path a person could open rather than
58/// wherever the build script happened to find it.
59///
60/// # Errors
61///
62/// A guard this module cannot compile, reported with the position of the term that was not
63/// understood. Every other way a rule set can be wrong has been reported by the reader or by
64/// the trie before anything gets here.
65pub fn emit(source: &str, rules: &[Rule], matcher: &Matcher) -> Result<String, Vec<Error>> {
66    let mut out = String::new();
67    let mut errors = Vec::new();
68    let mut wanted: Vec<&'static str> = Vec::new();
69
70    let guards = compile_guards(source, rules, &mut wanted, &mut errors);
71    if !errors.is_empty() {
72        return Err(errors);
73    }
74
75    header(&mut out, source, rules, matcher);
76    nodes(&mut out, matcher);
77    replacements(&mut out, source, rules, &guards);
78    out.push_str(&guards.iter().flatten().map(String::as_str).collect::<String>());
79    helpers(&mut out, &wanted);
80    Ok(out)
81}
82
83/// The comment nobody reads until they have to, and the table itself.
84fn header(out: &mut String, source: &str, rules: &[Rule], matcher: &Matcher) {
85    let _ = write!(
86        out,
87        "\
88// Generated from {source} by rucc-rules. Do not edit this file: edit the
89// rule file and build again. It holds {} rules over {} trie nodes.
90//
91// The types are the ones the module that includes this file defines, and the walk over the
92// table is there too. What is here is the table.
93
94use super::{{Node, Piece, Rule, Table, Test}};
95
96/// The rule file this table was built from, so that anything said about a rule can name a file
97/// somebody can open.
98pub const SOURCE: &str = {source:?};
99
100/// The rules of this file, as an automaton over their patterns.
101pub static TABLE: Table = Table {{ source: SOURCE, nodes: NODES, rules: RULES }};
102",
103        rules.len(),
104        matcher.nodes.len()
105    );
106}
107
108/// The trie, one array entry per node, with node zero the root.
109fn nodes(out: &mut String, matcher: &Matcher) {
110    out.push_str(
111        "\n/// The trie over the patterns. A node holds the tests to try in order, the branch\n\
112         /// that takes anything, and the rule that ends here if one does.\nstatic NODES: \
113         &[Node] = &[\n",
114    );
115    for (index, node) in matcher.nodes.iter().enumerate() {
116        let _ = writeln!(out, "    // {index}");
117        out.push_str("    Node {\n        tests: &[");
118        for (test, next) in &node.tests {
119            match test {
120                Test::App { head, arity } => {
121                    let _ = write!(
122                        out,
123                        "\n            (Test::App {{ head: {head:?}, arity: {arity} }}, {next}),"
124                    );
125                }
126                Test::Int(value) => {
127                    let _ = write!(out, "\n            (Test::Int({value}), {next}),");
128                }
129                Test::Same(index) => {
130                    let _ = write!(out, "\n            (Test::Same({index}), {next}),");
131                }
132            }
133        }
134        if !node.tests.is_empty() {
135            out.push_str("\n        ");
136        }
137        out.push_str("],\n");
138        match &node.wildcard {
139            Some((name, next)) => {
140                let _ = writeln!(out, "        wildcard: Some(({name:?}, {next})),");
141            }
142            None => out.push_str("        wildcard: None,\n"),
143        }
144        match node.accept {
145            Some(rule) => {
146                let _ = writeln!(out, "        accept: Some({rule}),");
147            }
148            None => out.push_str("        accept: None,\n"),
149        }
150        out.push_str("    },\n");
151    }
152    out.push_str("];\n");
153}
154
155/// The rules, one array entry each, in the order the file writes them.
156fn replacements(out: &mut String, source: &str, rules: &[Rule], guards: &[Option<String>]) {
157    out.push_str(
158        "\n/// The rules, in the order the rule file writes them, which is the order the\n\
159         /// `accept` of a trie node names.\nstatic RULES: &[Rule] = &[\n",
160    );
161    for (index, rule) in rules.iter().enumerate() {
162        let pattern = rule.pattern.to_string();
163        let _ = writeln!(out, "    // {source}:{}", rule.line);
164        out.push_str("    Rule {\n");
165        let _ = writeln!(out, "        pattern: {pattern:?},");
166        out.push_str("        replacement: &[");
167        let bound = bound_names(&rule.pattern);
168        for piece in pieces(&rule.replacement, &bound) {
169            let _ = write!(out, "\n            {piece},");
170        }
171        out.push_str("\n        ],\n");
172        match guards[index] {
173            Some(_) => {
174                let _ = writeln!(out, "        guard: Some(guard_{index}),");
175            }
176            None => out.push_str("        guard: None,\n"),
177        }
178        let _ = writeln!(out, "        line: {},", rule.line);
179        out.push_str("    },\n");
180    }
181    out.push_str("];\n");
182}
183
184/// The names a pattern binds, in the order the matcher binds them, which is the pre-order it
185/// walks the subject in. A replacement names one of them and the table holds the position,
186/// because a position is what the match has and a name is what the reader has.
187///
188/// A name written twice binds once. The second occurrence is a test that the two places hold the
189/// same thing rather than a second hole, so it takes no position, and counting it here would put
190/// every later name one place along from where the match actually holds it.
191fn bound_names(pattern: &Term) -> Vec<String> {
192    let mut out: Vec<String> = Vec::new();
193    pattern.walk(&mut |term| {
194        if let TermKind::Var(name) = &term.kind {
195            if !out.iter().any(|have| have == name) {
196                out.push(name.clone());
197            }
198        }
199    });
200    out
201}
202
203/// One replacement term, flattened into the pieces that build it, in pre-order.
204fn pieces(term: &Term, bound: &[String]) -> Vec<String> {
205    let mut out = Vec::new();
206    push_pieces(term, bound, &mut out);
207    out
208}
209
210fn push_pieces(term: &Term, bound: &[String], out: &mut Vec<String>) {
211    match &term.kind {
212        TermKind::Var(name) => {
213            // The reader has already refused a replacement naming something the pattern never
214            // bound, so there is a position for every name that reaches here.
215            let index = bound.iter().position(|have| have == name).unwrap_or_default();
216            out.push(format!("Piece::Var {{ name: {name:?}, index: {index} }}"));
217        }
218        TermKind::Int(value) => out.push(format!("Piece::Int({value})")),
219        TermKind::App { head, args } => {
220            out.push(format!("Piece::App {{ head: {head:?}, arity: {} }}", args.len()));
221            for arg in args {
222                push_pieces(arg, bound, out);
223            }
224        }
225    }
226}
227
228/// One function per guarded rule, or nothing for a rule with no guard.
229fn compile_guards(
230    source: &str,
231    rules: &[Rule],
232    wanted: &mut Vec<&'static str>,
233    errors: &mut Vec<Error>,
234) -> Vec<Option<String>> {
235    let mut out = Vec::with_capacity(rules.len());
236    for (index, rule) in rules.iter().enumerate() {
237        let Some(guard) = &rule.guard else {
238            out.push(None);
239            continue;
240        };
241        let bound = bound_names(&rule.pattern);
242        let mut used = Vec::new();
243        let condition = match condition(source, guard, &bound, wanted, &mut used) {
244            Ok(text) => text,
245            Err(error) => {
246                errors.push(error);
247                out.push(None);
248                continue;
249            }
250        };
251        let mut text = format!(
252            "\n/// `{guard}`, which is the guard of the rule on line {}.\nfn guard_{index}(bound: \
253             &[Option<i128>]) -> bool {{\n",
254            rule.line
255        );
256        used.sort_unstable();
257        used.dedup();
258        for at in used {
259            let _ = writeln!(
260                text,
261                "    // {}\n    let Some(Some(v{at})) = bound.get({at}).copied() else {{ return \
262                 false }};",
263                bound[at]
264            );
265        }
266        let _ = writeln!(text, "    {}\n}}", bare(&condition));
267        out.push(Some(text));
268    }
269    out
270}
271
272/// An expression without the parentheses that wrap the whole of it.
273///
274/// Every condition is emitted parenthesised, because an operand of one has to be. The outermost
275/// one is nobody's operand, and Rust warns about the parentheses around it, which in a generated
276/// file is a warning the reader of it can do nothing with.
277fn bare(text: &str) -> &str {
278    let Some(inner) = text.strip_prefix('(').and_then(|text| text.strip_suffix(')')) else {
279        return text;
280    };
281    let mut depth = 0i32;
282    for c in inner.chars() {
283        match c {
284            '(' => depth += 1,
285            ')' => depth -= 1,
286            _ => {}
287        }
288        // The pair that opened the string closed before the end of it, so the two ends are not
289        // a pair and taking them off would be taking off two different people's parentheses.
290        if depth < 0 {
291            return text;
292        }
293    }
294    inner
295}
296
297/// A guard as a Rust expression of type `bool`.
298fn condition(
299    source: &str,
300    term: &Term,
301    bound: &[String],
302    wanted: &mut Vec<&'static str>,
303    used: &mut Vec<usize>,
304) -> Result<String, Error> {
305    let TermKind::App { head, args } = &term.kind else {
306        return Err(refused(source, term, "a guard is a condition, and this is not one"));
307    };
308    let arity = args.len();
309    match (head.as_str(), arity) {
310        ("and" | "or", 1..) => {
311            let joint = if head == "and" { " && " } else { " || " };
312            let mut parts = Vec::with_capacity(arity);
313            for arg in args {
314                parts.push(condition(source, arg, bound, wanted, used)?);
315            }
316            Ok(format!("({})", parts.join(joint)))
317        }
318        ("not", 1) => Ok(format!("!{}", condition(source, &args[0], bound, wanted, used)?)),
319        ("=" | "!=" | "<" | "<=" | ">" | ">=", 2) => {
320            let operator = if head == "=" { "==" } else { head.as_str() };
321            let left = value(source, &args[0], bound, wanted, used)?;
322            let right = value(source, &args[1], bound, wanted, used)?;
323            Ok(format!("({left} {operator} {right})"))
324        }
325        _ => Err(refused(
326            source,
327            term,
328            &format!(
329                "`{head}` of {arity} is not a condition a guard can be compiled to. A guard is \
330                 `and`, `or`, `not`, or a comparison of two numbers"
331            ),
332        )),
333    }
334}
335
336/// A term inside a guard that stands for a number.
337fn value(
338    source: &str,
339    term: &Term,
340    bound: &[String],
341    wanted: &mut Vec<&'static str>,
342    used: &mut Vec<usize>,
343) -> Result<String, Error> {
344    match &term.kind {
345        TermKind::Int(number) => Ok(format!("{number}")),
346        TermKind::Var(name) => {
347            // The reader has already refused a guard naming something the pattern never bound.
348            let at = bound.iter().position(|have| have == name).unwrap_or_default();
349            used.push(at);
350            Ok(format!("v{at}"))
351        }
352        TermKind::App { head, args } => {
353            let arity = args.len();
354            match (head.as_str(), arity) {
355                ("sign_extend" | "zero_extend" | "extract", 3) => {
356                    let first = width(source, &args[0])?;
357                    let second = width(source, &args[1])?;
358                    let inner = value(source, &args[2], bound, wanted, used)?;
359                    let name = match head.as_str() {
360                        "sign_extend" => "sign_extend",
361                        "zero_extend" => "zero_extend",
362                        _ => "extract",
363                    };
364                    want(wanted, name);
365                    Ok(format!("{name}({first}, {second}, {inner})"))
366                }
367                _ => Err(refused(
368                    source,
369                    term,
370                    &format!(
371                        "`{head}` of {arity} is not a number a guard can be compiled to. The \
372                         ones that are are `sign_extend`, `zero_extend` and `extract`"
373                    ),
374                )),
375            }
376        }
377    }
378}
379
380/// A width, which has to be written out rather than computed, because it is how many bits a
381/// machine instruction has room for and not something a program is allowed to vary.
382fn width(source: &str, term: &Term) -> Result<String, Error> {
383    match &term.kind {
384        TermKind::Int(number) if (0..=128).contains(number) => Ok(format!("{number}")),
385        _ => Err(refused(source, term, "a width has to be a number from 0 to 128")),
386    }
387}
388
389/// Remember a helper, and everything it is written in terms of.
390fn want(wanted: &mut Vec<&'static str>, name: &'static str) {
391    if wanted.contains(&name) {
392        return;
393    }
394    wanted.push(name);
395    match name {
396        "sign_extend" => want(wanted, "shifted"),
397        "zero_extend" | "extract" => want(wanted, "low"),
398        _ => {}
399    }
400}
401
402/// The helpers the guards used, in a fixed order so that the file does not move about between
403/// builds for no reason.
404fn helpers(out: &mut String, wanted: &[&str]) {
405    for (name, text) in HELPERS {
406        if wanted.contains(name) {
407            out.push_str(text);
408        }
409    }
410}
411
412fn refused(source: &str, term: &Term, message: &str) -> Error {
413    Error {
414        path: source.to_owned(),
415        line: term.line,
416        column: term.column,
417        message: message.to_owned(),
418    }
419}
420
421const SIGN_EXTEND: &str = "
422/// The low `from` bits of `value`, sign extended to `to` bits.
423fn sign_extend(from: u32, to: u32, value: i128) -> i128 {
424    shifted(to, shifted(from, value))
425}
426";
427
428const ZERO_EXTEND: &str = "
429/// The low `from` bits of `value`, read as a number and not sign extended.
430fn zero_extend(from: u32, to: u32, value: i128) -> i128 {
431    low(to, low(from, value))
432}
433";
434
435const EXTRACT: &str = "
436/// The bits from `hi` down to `lo` of `value`, read as a number.
437fn extract(hi: u32, lo: u32, value: i128) -> i128 {
438    if lo >= 128 || hi < lo {
439        return 0;
440    }
441    low(hi - lo + 1, value >> lo)
442}
443";
444
445const SHIFTED: &str = "
446/// `value` read as a signed number that many bits wide.
447fn shifted(bits: u32, value: i128) -> i128 {
448    match 128u32.checked_sub(bits) {
449        Some(room) if room > 0 => (value << room) >> room,
450        _ => value,
451    }
452}
453";
454
455const LOW: &str = "
456/// The low `bits` bits of `value`, read as a number.
457fn low(bits: u32, value: i128) -> i128 {
458    if bits >= 128 {
459        return value;
460    }
461    #[allow(clippy::cast_possible_wrap)]
462    let masked = (value as u128 & ((1u128 << bits) - 1)) as i128;
463    masked
464}
465";
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::parse;
471
472    fn built(text: &str) -> String {
473        let rules = parse("rules/test.rules", text).expect("the rules read");
474        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
475        emit("rules/test.rules", &rules, &matcher).expect("the table is emitted")
476    }
477
478    /// The shape of the file, which is what the module that includes it is written against.
479    #[test]
480    fn a_rule_set_comes_out_as_a_table_of_nodes_and_a_table_of_rules() {
481        let out = built(
482            "(rule (lower (add.i64 (value.i64 x) (value.i64 y)))\n\
483             (x64.add_rr_64 x y)\n\
484             (spec (= (bvadd x y) (result))))\n",
485        );
486        assert!(out.contains("use super::{Node, Piece, Rule, Table, Test};"), "{out}");
487        assert!(out.contains("pub const SOURCE: &str = \"rules/test.rules\";"), "{out}");
488        assert!(out.contains("(Test::App { head: \"add.i64\", arity: 2 }, 1),"), "{out}");
489        assert!(out.contains("wildcard: Some((\"x\", 3)),"), "{out}");
490        assert!(out.contains("accept: Some(0),"), "{out}");
491        assert!(out.contains("Piece::App { head: \"x64.add_rr_64\", arity: 2 }"), "{out}");
492        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
493        assert!(out.contains("Piece::Var { name: \"y\", index: 1 }"), "{out}");
494        assert!(out.contains("guard: None,"), "{out}");
495    }
496
497    /// A name written twice comes out as a test and not as a second hole, so the positions a
498    /// replacement and a guard are written against count it once. Here `k` is binding one, which
499    /// it would not be if the second `x` had taken a position of its own.
500    #[test]
501    fn a_name_written_twice_comes_out_as_a_test_and_takes_no_position() {
502        let out = built(
503            "(rule (simplify (and.i32 (value.i32 x) (value.i32 x)))\n\
504             (value.i32 x)\n\
505             (spec (= x (result))))\n\
506             (rule (simplify (shl.i32 (value.i32 x) (iconst.i32 k)))\n\
507             (if (>= k 0))\n\
508             (value.i32 x)\n\
509             (spec (= (bvshl x k) (result))))\n",
510        );
511        assert!(out.contains("(Test::Same(0), "), "{out}");
512        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
513        assert!(
514            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
515            "{out}"
516        );
517    }
518
519    /// A guard becomes a function of the constants the pattern matched, and the helpers it
520    /// calls come with it. A binding it reads that is not a constant makes it false, which is
521    /// what the `let ... else` in it is for.
522    #[test]
523    fn a_guard_comes_out_as_a_function_of_the_bindings() {
524        let out = built(
525            "(rule (lower (shl.i64 (value.i64 x) (iconst.i64 k)))\n\
526             (if (and (>= k 0) (< k 64)))\n\
527             (x64.shl_ri_64 x k)\n\
528             (spec (= (bvshl x k) (result))))\n",
529        );
530        assert!(out.contains("guard: Some(guard_0),"), "{out}");
531        assert!(out.contains("fn guard_0(bound: &[Option<i128>]) -> bool {"), "{out}");
532        assert!(
533            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
534            "{out}"
535        );
536        assert!(out.contains("(v1 >= 0) && (v1 < 64)"), "{out}");
537        // Nothing this guard does not use is emitted, because an unused function in a
538        // generated file is a warning in the crate that includes it.
539        assert!(!out.contains("fn sign_extend"), "{out}");
540        assert!(!out.contains("fn low"), "{out}");
541    }
542
543    /// The immediate guard, which is the one that needs the arithmetic helpers, and which is
544    /// what pulls `shifted` and `low` in behind them.
545    #[test]
546    fn a_guard_that_reads_bits_brings_the_helpers_it_needs() {
547        let out = built(
548            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
549             (if (= k (sign_extend 32 64 (extract 31 0 k))))\n\
550             (x64.add_ri_64 x k)\n\
551             (spec (= (bvadd x k) (result))))\n",
552        );
553        assert!(out.contains("v1 == sign_extend(32, 64, extract(31, 0, v1))"), "{out}");
554        assert!(out.contains("fn sign_extend(from: u32, to: u32, value: i128) -> i128 {"), "{out}");
555        assert!(out.contains("fn shifted(bits: u32, value: i128) -> i128 {"), "{out}");
556        assert!(out.contains("fn extract(hi: u32, lo: u32, value: i128) -> i128 {"), "{out}");
557        assert!(out.contains("fn low(bits: u32, value: i128) -> i128 {"), "{out}");
558        assert!(!out.contains("fn zero_extend"), "{out}");
559    }
560
561    /// A guard written in something this module does not compile is refused here, with the
562    /// position of the term, rather than emitted and found later as a compile error in a
563    /// generated file that nobody wrote.
564    #[test]
565    fn a_guard_nothing_can_be_made_of_is_refused_where_it_is_written() {
566        let rules = parse(
567            "rules/test.rules",
568            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
569             (if (fits_in_a_byte k))\n\
570             (x64.add_ri_64 x k)\n\
571             (spec (= (bvadd x k) (result))))\n",
572        )
573        .expect("the rules read");
574        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
575        let errors = emit("rules/test.rules", &rules, &matcher).expect_err("the guard is refused");
576        assert_eq!(errors.len(), 1);
577        assert_eq!(errors[0].line, 2);
578        assert!(
579            errors[0].message.contains("`fits_in_a_byte` of 1 is not a condition"),
580            "{}",
581            errors[0]
582        );
583    }
584}