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