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        // The condition comes out in the order the rule file writes it, so that a reader can hold
252        // the two side by side. That is what the lint is turned off for: `(>= k 0)` and `(< k 64)`
253        // are two conditions in the rule and `(0..64).contains(&k)` is not either of them.
254        let mut text = format!(
255            "\n/// `{guard}`, which is the guard of the rule on line {}.\n\
256             #[allow(clippy::manual_range_contains)]\nfn guard_{index}(bound: \
257             &[Option<i128>]) -> bool {{\n",
258            rule.line
259        );
260        used.sort_unstable();
261        used.dedup();
262        for at in used {
263            let _ = writeln!(
264                text,
265                "    // {}\n    let Some(Some(v{at})) = bound.get({at}).copied() else {{ return \
266                 false }};",
267                bound[at]
268            );
269        }
270        let _ = writeln!(text, "    {}\n}}", bare(&condition));
271        out.push(Some(text));
272    }
273    out
274}
275
276/// An expression without the parentheses that wrap the whole of it.
277///
278/// Every condition is emitted parenthesised, because an operand of one has to be. The outermost
279/// one is nobody's operand, and Rust warns about the parentheses around it, which in a generated
280/// file is a warning the reader of it can do nothing with.
281fn bare(text: &str) -> &str {
282    let Some(inner) = text.strip_prefix('(').and_then(|text| text.strip_suffix(')')) else {
283        return text;
284    };
285    let mut depth = 0i32;
286    for c in inner.chars() {
287        match c {
288            '(' => depth += 1,
289            ')' => depth -= 1,
290            _ => {}
291        }
292        // The pair that opened the string closed before the end of it, so the two ends are not
293        // a pair and taking them off would be taking off two different people's parentheses.
294        if depth < 0 {
295            return text;
296        }
297    }
298    inner
299}
300
301/// A guard as a Rust expression of type `bool`.
302fn condition(
303    source: &str,
304    term: &Term,
305    bound: &[String],
306    wanted: &mut Vec<&'static str>,
307    used: &mut Vec<usize>,
308) -> Result<String, Error> {
309    let TermKind::App { head, args } = &term.kind else {
310        return Err(refused(source, term, "a guard is a condition, and this is not one"));
311    };
312    let arity = args.len();
313    match (head.as_str(), arity) {
314        ("and" | "or", 1..) => {
315            let joint = if head == "and" { " && " } else { " || " };
316            let mut parts = Vec::with_capacity(arity);
317            for arg in args {
318                parts.push(condition(source, arg, bound, wanted, used)?);
319            }
320            Ok(format!("({})", parts.join(joint)))
321        }
322        ("not", 1) => Ok(format!("!{}", condition(source, &args[0], bound, wanted, used)?)),
323        ("=" | "!=" | "<" | "<=" | ">" | ">=", 2) => {
324            let operator = if head == "=" { "==" } else { head.as_str() };
325            let left = value(source, &args[0], bound, wanted, used)?;
326            let right = value(source, &args[1], bound, wanted, used)?;
327            Ok(format!("({left} {operator} {right})"))
328        }
329        _ => Err(refused(
330            source,
331            term,
332            &format!(
333                "`{head}` of {arity} is not a condition a guard can be compiled to. A guard is \
334                 `and`, `or`, `not`, or a comparison of two numbers"
335            ),
336        )),
337    }
338}
339
340/// A term inside a guard that stands for a number.
341fn value(
342    source: &str,
343    term: &Term,
344    bound: &[String],
345    wanted: &mut Vec<&'static str>,
346    used: &mut Vec<usize>,
347) -> Result<String, Error> {
348    match &term.kind {
349        TermKind::Int(number) => Ok(format!("{number}")),
350        TermKind::Var(name) => {
351            // The reader has already refused a guard naming something the pattern never bound.
352            let at = bound.iter().position(|have| have == name).unwrap_or_default();
353            used.push(at);
354            Ok(format!("v{at}"))
355        }
356        TermKind::App { head, args } => {
357            let arity = args.len();
358            match (head.as_str(), arity) {
359                // Adding and subtracting, which is what a guard about two offsets into one object
360                // is written in.
361                //
362                // Saturating rather than plain, because a guard is a condition on whatever
363                // constants the match happened to hold and there is nothing to stop those being
364                // the ends of the type. Plain arithmetic there is a panic in a debug build and a
365                // wrap in a release one, and neither is an answer to a question about a rule.
366                //
367                // Saturating is not the solver's arithmetic either. The solver reads a guard in
368                // the width the rule runs at, where adding wraps, and this reads it in `i128`,
369                // where it does not. The two agree exactly while the operands stay small, so a
370                // rule that adds says how small in the same guard, and one that does not is a
371                // rule proved about arithmetic the compiler is not doing.
372                ("+" | "-", 2) => {
373                    let left = value(source, &args[0], bound, wanted, used)?;
374                    let right = value(source, &args[1], bound, wanted, used)?;
375                    let name = if head == "+" { "saturating_add" } else { "saturating_sub" };
376                    Ok(format!("({left}).{name}({right})"))
377                }
378                ("sign_extend" | "zero_extend" | "extract", 3) => {
379                    let first = width(source, &args[0])?;
380                    let second = width(source, &args[1])?;
381                    let inner = value(source, &args[2], bound, wanted, used)?;
382                    let name = match head.as_str() {
383                        "sign_extend" => "sign_extend",
384                        "zero_extend" => "zero_extend",
385                        _ => "extract",
386                    };
387                    want(wanted, name);
388                    Ok(format!("{name}({first}, {second}, {inner})"))
389                }
390                _ => Err(refused(
391                    source,
392                    term,
393                    &format!(
394                        "`{head}` of {arity} is not a number a guard can be compiled to. The \
395                         ones that are are `+`, `-`, `sign_extend`, `zero_extend` and `extract`"
396                    ),
397                )),
398            }
399        }
400    }
401}
402
403/// A width, which has to be written out rather than computed, because it is how many bits a
404/// machine instruction has room for and not something a program is allowed to vary.
405fn width(source: &str, term: &Term) -> Result<String, Error> {
406    match &term.kind {
407        TermKind::Int(number) if (0..=128).contains(number) => Ok(format!("{number}")),
408        _ => Err(refused(source, term, "a width has to be a number from 0 to 128")),
409    }
410}
411
412/// Remember a helper, and everything it is written in terms of.
413fn want(wanted: &mut Vec<&'static str>, name: &'static str) {
414    if wanted.contains(&name) {
415        return;
416    }
417    wanted.push(name);
418    match name {
419        "sign_extend" => want(wanted, "shifted"),
420        "zero_extend" | "extract" => want(wanted, "low"),
421        _ => {}
422    }
423}
424
425/// The helpers the guards used, in a fixed order so that the file does not move about between
426/// builds for no reason.
427fn helpers(out: &mut String, wanted: &[&str]) {
428    for (name, text) in HELPERS {
429        if wanted.contains(name) {
430            out.push_str(text);
431        }
432    }
433}
434
435fn refused(source: &str, term: &Term, message: &str) -> Error {
436    Error {
437        path: source.to_owned(),
438        line: term.line,
439        column: term.column,
440        message: message.to_owned(),
441    }
442}
443
444const SIGN_EXTEND: &str = "
445/// The low `from` bits of `value`, sign extended to `to` bits.
446fn sign_extend(from: u32, to: u32, value: i128) -> i128 {
447    shifted(to, shifted(from, value))
448}
449";
450
451const ZERO_EXTEND: &str = "
452/// The low `from` bits of `value`, read as a number and not sign extended.
453fn zero_extend(from: u32, to: u32, value: i128) -> i128 {
454    low(to, low(from, value))
455}
456";
457
458const EXTRACT: &str = "
459/// The bits from `hi` down to `lo` of `value`, read as a number.
460fn extract(hi: u32, lo: u32, value: i128) -> i128 {
461    if lo >= 128 || hi < lo {
462        return 0;
463    }
464    low(hi - lo + 1, value >> lo)
465}
466";
467
468const SHIFTED: &str = "
469/// `value` read as a signed number that many bits wide.
470fn shifted(bits: u32, value: i128) -> i128 {
471    match 128u32.checked_sub(bits) {
472        Some(room) if room > 0 => (value << room) >> room,
473        _ => value,
474    }
475}
476";
477
478const LOW: &str = "
479/// The low `bits` bits of `value`, read as a number.
480fn low(bits: u32, value: i128) -> i128 {
481    if bits >= 128 {
482        return value;
483    }
484    #[allow(clippy::cast_possible_wrap)]
485    let masked = (value as u128 & ((1u128 << bits) - 1)) as i128;
486    masked
487}
488";
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::parse;
494
495    fn built(text: &str) -> String {
496        let rules = parse("rules/test.rules", text).expect("the rules read");
497        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
498        emit("rules/test.rules", &rules, &matcher).expect("the table is emitted")
499    }
500
501    /// The shape of the file, which is what the module that includes it is written against.
502    #[test]
503    fn a_rule_set_comes_out_as_a_table_of_nodes_and_a_table_of_rules() {
504        let out = built(
505            "(rule (lower (add.i64 (value.i64 x) (value.i64 y)))\n\
506             (x64.add_rr_64 x y)\n\
507             (spec (= (bvadd x y) (result))))\n",
508        );
509        assert!(out.contains("use super::{Node, Piece, Rule, Table, Test};"), "{out}");
510        assert!(out.contains("pub const SOURCE: &str = \"rules/test.rules\";"), "{out}");
511        assert!(out.contains("(Test::App { head: \"add.i64\", arity: 2 }, 1),"), "{out}");
512        assert!(out.contains("wildcard: Some((\"x\", 3)),"), "{out}");
513        assert!(out.contains("accept: Some(0),"), "{out}");
514        assert!(out.contains("Piece::App { head: \"x64.add_rr_64\", arity: 2 }"), "{out}");
515        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
516        assert!(out.contains("Piece::Var { name: \"y\", index: 1 }"), "{out}");
517        assert!(out.contains("guard: None,"), "{out}");
518    }
519
520    /// A name written twice comes out as a test and not as a second hole, so the positions a
521    /// replacement and a guard are written against count it once. Here `k` is binding one, which
522    /// it would not be if the second `x` had taken a position of its own.
523    #[test]
524    fn a_name_written_twice_comes_out_as_a_test_and_takes_no_position() {
525        let out = built(
526            "(rule (simplify (and.i32 (value.i32 x) (value.i32 x)))\n\
527             (value.i32 x)\n\
528             (spec (= x (result))))\n\
529             (rule (simplify (shl.i32 (value.i32 x) (iconst.i32 k)))\n\
530             (if (>= k 0))\n\
531             (value.i32 x)\n\
532             (spec (= (bvshl x k) (result))))\n",
533        );
534        assert!(out.contains("(Test::Same(0), "), "{out}");
535        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
536        assert!(
537            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
538            "{out}"
539        );
540    }
541
542    /// A guard becomes a function of the constants the pattern matched, and the helpers it
543    /// calls come with it. A binding it reads that is not a constant makes it false, which is
544    /// what the `let ... else` in it is for.
545    #[test]
546    fn a_guard_comes_out_as_a_function_of_the_bindings() {
547        let out = built(
548            "(rule (lower (shl.i64 (value.i64 x) (iconst.i64 k)))\n\
549             (if (and (>= k 0) (< k 64)))\n\
550             (x64.shl_ri_64 x k)\n\
551             (spec (= (bvshl x k) (result))))\n",
552        );
553        assert!(out.contains("guard: Some(guard_0),"), "{out}");
554        assert!(out.contains("fn guard_0(bound: &[Option<i128>]) -> bool {"), "{out}");
555        assert!(
556            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
557            "{out}"
558        );
559        assert!(out.contains("(v1 >= 0) && (v1 < 64)"), "{out}");
560        // Nothing this guard does not use is emitted, because an unused function in a
561        // generated file is a warning in the crate that includes it.
562        assert!(!out.contains("fn sign_extend"), "{out}");
563        assert!(!out.contains("fn low"), "{out}");
564    }
565
566    /// The immediate guard, which is the one that needs the arithmetic helpers, and which is
567    /// what pulls `shifted` and `low` in behind them.
568    #[test]
569    fn a_guard_that_reads_bits_brings_the_helpers_it_needs() {
570        let out = built(
571            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
572             (if (= k (sign_extend 32 64 (extract 31 0 k))))\n\
573             (x64.add_ri_64 x k)\n\
574             (spec (= (bvadd x k) (result))))\n",
575        );
576        assert!(out.contains("v1 == sign_extend(32, 64, extract(31, 0, v1))"), "{out}");
577        assert!(out.contains("fn sign_extend(from: u32, to: u32, value: i128) -> i128 {"), "{out}");
578        assert!(out.contains("fn shifted(bits: u32, value: i128) -> i128 {"), "{out}");
579        assert!(out.contains("fn extract(hi: u32, lo: u32, value: i128) -> i128 {"), "{out}");
580        assert!(out.contains("fn low(bits: u32, value: i128) -> i128 {"), "{out}");
581        assert!(!out.contains("fn zero_extend"), "{out}");
582    }
583
584    /// A guard written in something this module does not compile is refused here, with the
585    /// position of the term, rather than emitted and found later as a compile error in a
586    /// generated file that nobody wrote.
587    #[test]
588    fn a_guard_nothing_can_be_made_of_is_refused_where_it_is_written() {
589        let rules = parse(
590            "rules/test.rules",
591            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
592             (if (fits_in_a_byte k))\n\
593             (x64.add_ri_64 x k)\n\
594             (spec (= (bvadd x k) (result))))\n",
595        )
596        .expect("the rules read");
597        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
598        let errors = emit("rules/test.rules", &rules, &matcher).expect_err("the guard is refused");
599        assert_eq!(errors.len(), 1);
600        assert_eq!(errors[0].line, 2);
601        assert!(
602            errors[0].message.contains("`fits_in_a_byte` of 1 is not a condition"),
603            "{}",
604            errors[0]
605        );
606    }
607}