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;
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 shape = matcher.shape();
86    let _ = write!(
87        out,
88        "\
89// Generated from {source} by rucc-rules. Do not edit this file: edit the
90// rule file and build again. It holds {} rules over {} trie nodes.
91//
92// The widest node has {} branches. Reading them in the order the rules are written would ask
93// that many questions to reach the last of them and to find that none of them matched, and the
94// search that is done instead asks {}. {} nodes ask more than one kind of question, which is how
95// many of them the order the kinds are tried in decides anything at.
96//
97// The types are the ones the module that includes this file defines, and the walk over the
98// table is there too. What is here is the table.
99
100use super::{{Node, Piece, Rule, Table}};
101
102/// The rule file this table was built from, so that anything said about a rule can name a file
103/// somebody can open.
104pub const SOURCE: &str = {source:?};
105
106/// The rules of this file, as an automaton over their patterns.
107pub static TABLE: Table = Table {{ source: SOURCE, nodes: NODES, rules: RULES }};
108",
109        rules.len(),
110        shape.nodes,
111        shape.widest,
112        shape.search,
113        shape.mixed,
114    );
115}
116
117/// The trie, one array entry per node, with node zero the root.
118fn nodes(out: &mut String, matcher: &Matcher) {
119    out.push_str(
120        "\n/// The trie over the patterns. A node holds the branches taken on the head of a\n\
121         /// term, the branches taken on the value of a constant, the branches taken on a\n\
122         /// repeat of an earlier binding, the branch that takes anything, and the rule that\n\
123         /// ends here if one does. The first two are sorted, which is what makes finding a\n\
124         /// branch a search.\nstatic NODES: &[Node] = &[\n",
125    );
126    for (index, node) in matcher.nodes.iter().enumerate() {
127        let _ = writeln!(out, "    // {index}");
128        out.push_str("    Node {\n        heads: &[");
129        for (head, arity, next) in &node.heads {
130            let _ = write!(out, "\n            ({head:?}, {arity}, {next}),");
131        }
132        if !node.heads.is_empty() {
133            out.push_str("\n        ");
134        }
135        out.push_str("],\n        ints: &[");
136        for (value, next) in &node.ints {
137            let _ = write!(out, "\n            ({value}, {next}),");
138        }
139        if !node.ints.is_empty() {
140            out.push_str("\n        ");
141        }
142        out.push_str("],\n        same: &[");
143        for (binding, next) in &node.same {
144            let _ = write!(out, "\n            ({binding}, {next}),");
145        }
146        if !node.same.is_empty() {
147            out.push_str("\n        ");
148        }
149        out.push_str("],\n");
150        match &node.wildcard {
151            Some((name, next)) => {
152                let _ = writeln!(out, "        wildcard: Some(({name:?}, {next})),");
153            }
154            None => out.push_str("        wildcard: None,\n"),
155        }
156        match node.accept {
157            Some(rule) => {
158                let _ = writeln!(out, "        accept: Some({rule}),");
159            }
160            None => out.push_str("        accept: None,\n"),
161        }
162        out.push_str("    },\n");
163    }
164    out.push_str("];\n");
165}
166
167/// The rules, one array entry each, in the order the file writes them.
168fn replacements(out: &mut String, source: &str, rules: &[Rule], guards: &[Option<String>]) {
169    out.push_str(
170        "\n/// The rules, in the order the rule file writes them, which is the order the\n\
171         /// `accept` of a trie node names.\nstatic RULES: &[Rule] = &[\n",
172    );
173    for (index, rule) in rules.iter().enumerate() {
174        let pattern = rule.pattern.to_string();
175        let _ = writeln!(out, "    // {source}:{}", rule.line);
176        out.push_str("    Rule {\n");
177        let _ = writeln!(out, "        pattern: {pattern:?},");
178        out.push_str("        replacement: &[");
179        let bound = bound_names(&rule.pattern);
180        for piece in pieces(&rule.replacement, &bound) {
181            let _ = write!(out, "\n            {piece},");
182        }
183        out.push_str("\n        ],\n");
184        match guards[index] {
185            Some(_) => {
186                let _ = writeln!(out, "        guard: Some(guard_{index}),");
187            }
188            None => out.push_str("        guard: None,\n"),
189        }
190        let _ = writeln!(out, "        line: {},", rule.line);
191        out.push_str("    },\n");
192    }
193    out.push_str("];\n");
194}
195
196/// The names a pattern binds, in the order the matcher binds them, which is the pre-order it
197/// walks the subject in. A replacement names one of them and the table holds the position,
198/// because a position is what the match has and a name is what the reader has.
199///
200/// A name written twice binds once. The second occurrence is a test that the two places hold the
201/// same thing rather than a second hole, so it takes no position, and counting it here would put
202/// every later name one place along from where the match actually holds it.
203fn bound_names(pattern: &Term) -> Vec<String> {
204    let mut out: Vec<String> = Vec::new();
205    pattern.walk(&mut |term| {
206        if let TermKind::Var(name) = &term.kind {
207            if !out.iter().any(|have| have == name) {
208                out.push(name.clone());
209            }
210        }
211    });
212    out
213}
214
215/// One replacement term, flattened into the pieces that build it, in pre-order.
216fn pieces(term: &Term, bound: &[String]) -> Vec<String> {
217    let mut out = Vec::new();
218    push_pieces(term, bound, &mut out);
219    out
220}
221
222fn push_pieces(term: &Term, bound: &[String], out: &mut Vec<String>) {
223    match &term.kind {
224        TermKind::Var(name) => {
225            // The reader has already refused a replacement naming something the pattern never
226            // bound, so there is a position for every name that reaches here.
227            let index = bound.iter().position(|have| have == name).unwrap_or_default();
228            out.push(format!("Piece::Var {{ name: {name:?}, index: {index} }}"));
229        }
230        TermKind::Int(value) => out.push(format!("Piece::Int({value})")),
231        TermKind::App { head, args } => {
232            out.push(format!("Piece::App {{ head: {head:?}, arity: {} }}", args.len()));
233            for arg in args {
234                push_pieces(arg, bound, out);
235            }
236        }
237    }
238}
239
240/// One function per guarded rule, or nothing for a rule with no guard.
241fn compile_guards(
242    source: &str,
243    rules: &[Rule],
244    wanted: &mut Vec<&'static str>,
245    errors: &mut Vec<Error>,
246) -> Vec<Option<String>> {
247    let mut out = Vec::with_capacity(rules.len());
248    for (index, rule) in rules.iter().enumerate() {
249        let Some(guard) = &rule.guard else {
250            out.push(None);
251            continue;
252        };
253        let bound = bound_names(&rule.pattern);
254        let mut used = Vec::new();
255        let condition = match condition(source, guard, &bound, wanted, &mut used) {
256            Ok(text) => text,
257            Err(error) => {
258                errors.push(error);
259                out.push(None);
260                continue;
261            }
262        };
263        // The condition comes out in the order the rule file writes it, so that a reader can hold
264        // the two side by side. That is what the lint is turned off for: `(>= k 0)` and `(< k 64)`
265        // are two conditions in the rule and `(0..64).contains(&k)` is not either of them.
266        let mut text = format!(
267            "\n/// `{guard}`, which is the guard of the rule on line {}.\n\
268             #[allow(clippy::manual_range_contains)]\nfn guard_{index}(bound: \
269             &[Option<i128>]) -> bool {{\n",
270            rule.line
271        );
272        used.sort_unstable();
273        used.dedup();
274        for at in used {
275            let _ = writeln!(
276                text,
277                "    // {}\n    let Some(Some(v{at})) = bound.get({at}).copied() else {{ return \
278                 false }};",
279                bound[at]
280            );
281        }
282        let _ = writeln!(text, "    {}\n}}", bare(&condition));
283        out.push(Some(text));
284    }
285    out
286}
287
288/// An expression without the parentheses that wrap the whole of it.
289///
290/// Every condition is emitted parenthesised, because an operand of one has to be. The outermost
291/// one is nobody's operand, and Rust warns about the parentheses around it, which in a generated
292/// file is a warning the reader of it can do nothing with.
293fn bare(text: &str) -> &str {
294    let Some(inner) = text.strip_prefix('(').and_then(|text| text.strip_suffix(')')) else {
295        return text;
296    };
297    let mut depth = 0i32;
298    for c in inner.chars() {
299        match c {
300            '(' => depth += 1,
301            ')' => depth -= 1,
302            _ => {}
303        }
304        // The pair that opened the string closed before the end of it, so the two ends are not
305        // a pair and taking them off would be taking off two different people's parentheses.
306        if depth < 0 {
307            return text;
308        }
309    }
310    inner
311}
312
313/// A guard as a Rust expression of type `bool`.
314fn condition(
315    source: &str,
316    term: &Term,
317    bound: &[String],
318    wanted: &mut Vec<&'static str>,
319    used: &mut Vec<usize>,
320) -> Result<String, Error> {
321    let TermKind::App { head, args } = &term.kind else {
322        return Err(refused(source, term, "a guard is a condition, and this is not one"));
323    };
324    let arity = args.len();
325    match (head.as_str(), arity) {
326        ("and" | "or", 1..) => {
327            let joint = if head == "and" { " && " } else { " || " };
328            let mut parts = Vec::with_capacity(arity);
329            for arg in args {
330                parts.push(condition(source, arg, bound, wanted, used)?);
331            }
332            Ok(format!("({})", parts.join(joint)))
333        }
334        ("not", 1) => Ok(format!("!{}", condition(source, &args[0], bound, wanted, used)?)),
335        ("=" | "!=" | "<" | "<=" | ">" | ">=", 2) => {
336            let operator = if head == "=" { "==" } else { head.as_str() };
337            let left = value(source, &args[0], bound, wanted, used)?;
338            let right = value(source, &args[1], bound, wanted, used)?;
339            Ok(format!("({left} {operator} {right})"))
340        }
341        _ => Err(refused(
342            source,
343            term,
344            &format!(
345                "`{head}` of {arity} is not a condition a guard can be compiled to. A guard is \
346                 `and`, `or`, `not`, or a comparison of two numbers"
347            ),
348        )),
349    }
350}
351
352/// A term inside a guard that stands for a number.
353fn value(
354    source: &str,
355    term: &Term,
356    bound: &[String],
357    wanted: &mut Vec<&'static str>,
358    used: &mut Vec<usize>,
359) -> Result<String, Error> {
360    match &term.kind {
361        TermKind::Int(number) => Ok(format!("{number}")),
362        TermKind::Var(name) => {
363            // The reader has already refused a guard naming something the pattern never bound.
364            let at = bound.iter().position(|have| have == name).unwrap_or_default();
365            used.push(at);
366            Ok(format!("v{at}"))
367        }
368        TermKind::App { head, args } => {
369            let arity = args.len();
370            match (head.as_str(), arity) {
371                // Adding and subtracting, which is what a guard about two offsets into one object
372                // is written in.
373                //
374                // Saturating rather than plain, because a guard is a condition on whatever
375                // constants the match happened to hold and there is nothing to stop those being
376                // the ends of the type. Plain arithmetic there is a panic in a debug build and a
377                // wrap in a release one, and neither is an answer to a question about a rule.
378                //
379                // Saturating is not the solver's arithmetic either. The solver reads a guard in
380                // the width the rule runs at, where adding wraps, and this reads it in `i128`,
381                // where it does not. The two agree exactly while the operands stay small, so a
382                // rule that adds says how small in the same guard, and one that does not is a
383                // rule proved about arithmetic the compiler is not doing.
384                ("+" | "-", 2) => {
385                    let left = value(source, &args[0], bound, wanted, used)?;
386                    let right = value(source, &args[1], bound, wanted, used)?;
387                    let name = if head == "+" { "saturating_add" } else { "saturating_sub" };
388                    Ok(format!("({left}).{name}({right})"))
389                }
390                ("sign_extend" | "zero_extend" | "extract", 3) => {
391                    let first = width(source, &args[0])?;
392                    let second = width(source, &args[1])?;
393                    let inner = value(source, &args[2], bound, wanted, used)?;
394                    let name = match head.as_str() {
395                        "sign_extend" => "sign_extend",
396                        "zero_extend" => "zero_extend",
397                        _ => "extract",
398                    };
399                    want(wanted, name);
400                    Ok(format!("{name}({first}, {second}, {inner})"))
401                }
402                _ => Err(refused(
403                    source,
404                    term,
405                    &format!(
406                        "`{head}` of {arity} is not a number a guard can be compiled to. The \
407                         ones that are are `+`, `-`, `sign_extend`, `zero_extend` and `extract`"
408                    ),
409                )),
410            }
411        }
412    }
413}
414
415/// A width, which has to be written out rather than computed, because it is how many bits a
416/// machine instruction has room for and not something a program is allowed to vary.
417fn width(source: &str, term: &Term) -> Result<String, Error> {
418    match &term.kind {
419        TermKind::Int(number) if (0..=128).contains(number) => Ok(format!("{number}")),
420        _ => Err(refused(source, term, "a width has to be a number from 0 to 128")),
421    }
422}
423
424/// Remember a helper, and everything it is written in terms of.
425fn want(wanted: &mut Vec<&'static str>, name: &'static str) {
426    if wanted.contains(&name) {
427        return;
428    }
429    wanted.push(name);
430    match name {
431        "sign_extend" => want(wanted, "shifted"),
432        "zero_extend" | "extract" => want(wanted, "low"),
433        _ => {}
434    }
435}
436
437/// The helpers the guards used, in a fixed order so that the file does not move about between
438/// builds for no reason.
439fn helpers(out: &mut String, wanted: &[&str]) {
440    for (name, text) in HELPERS {
441        if wanted.contains(name) {
442            out.push_str(text);
443        }
444    }
445}
446
447fn refused(source: &str, term: &Term, message: &str) -> Error {
448    Error {
449        path: source.to_owned(),
450        line: term.line,
451        column: term.column,
452        message: message.to_owned(),
453    }
454}
455
456const SIGN_EXTEND: &str = "
457/// The low `from` bits of `value`, sign extended to `to` bits.
458fn sign_extend(from: u32, to: u32, value: i128) -> i128 {
459    shifted(to, shifted(from, value))
460}
461";
462
463const ZERO_EXTEND: &str = "
464/// The low `from` bits of `value`, read as a number and not sign extended.
465fn zero_extend(from: u32, to: u32, value: i128) -> i128 {
466    low(to, low(from, value))
467}
468";
469
470const EXTRACT: &str = "
471/// The bits from `hi` down to `lo` of `value`, read as a number.
472fn extract(hi: u32, lo: u32, value: i128) -> i128 {
473    if lo >= 128 || hi < lo {
474        return 0;
475    }
476    low(hi - lo + 1, value >> lo)
477}
478";
479
480const SHIFTED: &str = "
481/// `value` read as a signed number that many bits wide.
482fn shifted(bits: u32, value: i128) -> i128 {
483    match 128u32.checked_sub(bits) {
484        Some(room) if room > 0 => (value << room) >> room,
485        _ => value,
486    }
487}
488";
489
490const LOW: &str = "
491/// The low `bits` bits of `value`, read as a number.
492fn low(bits: u32, value: i128) -> i128 {
493    if bits >= 128 {
494        return value;
495    }
496    #[allow(clippy::cast_possible_wrap)]
497    let masked = (value as u128 & ((1u128 << bits) - 1)) as i128;
498    masked
499}
500";
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::parse;
506
507    fn built(text: &str) -> String {
508        let rules = parse("rules/test.rules", text).expect("the rules read");
509        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
510        emit("rules/test.rules", &rules, &matcher).expect("the table is emitted")
511    }
512
513    /// The shape of the file, which is what the module that includes it is written against.
514    #[test]
515    fn a_rule_set_comes_out_as_a_table_of_nodes_and_a_table_of_rules() {
516        let out = built(
517            "(rule (lower (add.i64 (value.i64 x) (value.i64 y)))\n\
518             (x64.add_rr_64 x y)\n\
519             (spec (= (bvadd x y) (result))))\n",
520        );
521        assert!(out.contains("use super::{Node, Piece, Rule, Table};"), "{out}");
522        assert!(out.contains("pub const SOURCE: &str = \"rules/test.rules\";"), "{out}");
523        assert!(out.contains("(\"add.i64\", 2, 1),"), "{out}");
524        assert!(out.contains("wildcard: Some((\"x\", 3)),"), "{out}");
525        assert!(out.contains("accept: Some(0),"), "{out}");
526        assert!(out.contains("Piece::App { head: \"x64.add_rr_64\", arity: 2 }"), "{out}");
527        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
528        assert!(out.contains("Piece::Var { name: \"y\", index: 1 }"), "{out}");
529        assert!(out.contains("guard: None,"), "{out}");
530    }
531
532    /// A name written twice comes out as a test and not as a second hole, so the positions a
533    /// replacement and a guard are written against count it once. Here `k` is binding one, which
534    /// it would not be if the second `x` had taken a position of its own.
535    #[test]
536    fn a_name_written_twice_comes_out_as_a_test_and_takes_no_position() {
537        let out = built(
538            "(rule (simplify (and.i32 (value.i32 x) (value.i32 x)))\n\
539             (value.i32 x)\n\
540             (spec (= x (result))))\n\
541             (rule (simplify (shl.i32 (value.i32 x) (iconst.i32 k)))\n\
542             (if (>= k 0))\n\
543             (value.i32 x)\n\
544             (spec (= (bvshl x k) (result))))\n",
545        );
546        assert!(out.contains("same: &[\n            (0, "), "{out}");
547        assert!(out.contains("Piece::Var { name: \"x\", index: 0 }"), "{out}");
548        assert!(
549            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
550            "{out}"
551        );
552    }
553
554    /// A guard becomes a function of the constants the pattern matched, and the helpers it
555    /// calls come with it. A binding it reads that is not a constant makes it false, which is
556    /// what the `let ... else` in it is for.
557    #[test]
558    fn a_guard_comes_out_as_a_function_of_the_bindings() {
559        let out = built(
560            "(rule (lower (shl.i64 (value.i64 x) (iconst.i64 k)))\n\
561             (if (and (>= k 0) (< k 64)))\n\
562             (x64.shl_ri_64 x k)\n\
563             (spec (= (bvshl x k) (result))))\n",
564        );
565        assert!(out.contains("guard: Some(guard_0),"), "{out}");
566        assert!(out.contains("fn guard_0(bound: &[Option<i128>]) -> bool {"), "{out}");
567        assert!(
568            out.contains("let Some(Some(v1)) = bound.get(1).copied() else { return false };"),
569            "{out}"
570        );
571        assert!(out.contains("(v1 >= 0) && (v1 < 64)"), "{out}");
572        // Nothing this guard does not use is emitted, because an unused function in a
573        // generated file is a warning in the crate that includes it.
574        assert!(!out.contains("fn sign_extend"), "{out}");
575        assert!(!out.contains("fn low"), "{out}");
576    }
577
578    /// The immediate guard, which is the one that needs the arithmetic helpers, and which is
579    /// what pulls `shifted` and `low` in behind them.
580    #[test]
581    fn a_guard_that_reads_bits_brings_the_helpers_it_needs() {
582        let out = built(
583            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
584             (if (= k (sign_extend 32 64 (extract 31 0 k))))\n\
585             (x64.add_ri_64 x k)\n\
586             (spec (= (bvadd x k) (result))))\n",
587        );
588        assert!(out.contains("v1 == sign_extend(32, 64, extract(31, 0, v1))"), "{out}");
589        assert!(out.contains("fn sign_extend(from: u32, to: u32, value: i128) -> i128 {"), "{out}");
590        assert!(out.contains("fn shifted(bits: u32, value: i128) -> i128 {"), "{out}");
591        assert!(out.contains("fn extract(hi: u32, lo: u32, value: i128) -> i128 {"), "{out}");
592        assert!(out.contains("fn low(bits: u32, value: i128) -> i128 {"), "{out}");
593        assert!(!out.contains("fn zero_extend"), "{out}");
594    }
595
596    /// A guard written in something this module does not compile is refused here, with the
597    /// position of the term, rather than emitted and found later as a compile error in a
598    /// generated file that nobody wrote.
599    #[test]
600    fn a_guard_nothing_can_be_made_of_is_refused_where_it_is_written() {
601        let rules = parse(
602            "rules/test.rules",
603            "(rule (lower (add.i64 (value.i64 x) (iconst.i64 k)))\n\
604             (if (fits_in_a_byte k))\n\
605             (x64.add_ri_64 x k)\n\
606             (spec (= (bvadd x k) (result))))\n",
607        )
608        .expect("the rules read");
609        let matcher = Matcher::build("rules/test.rules", &rules).expect("the matcher builds");
610        let errors = emit("rules/test.rules", &rules, &matcher).expect_err("the guard is refused");
611        assert_eq!(errors.len(), 1);
612        assert_eq!(errors[0].line, 2);
613        assert!(
614            errors[0].message.contains("`fits_in_a_byte` of 1 is not a condition"),
615            "{}",
616            errors[0]
617        );
618    }
619}