Skip to main content

rucc_driver/
compile.rs

1//! Running the front end over one file, from the bytes on disk to the typed tree.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.3, and the `M2` exit criterion in
4//! `spec/17-milestones.md` that says `--emit=tast` works.
5//!
6//! [`preprocess`](mod@crate::preprocess) stops after phase 4 because `-E` stops there. This
7//! carries on: phase 7, the parse, and the checking. It is one function rather than four composed
8//! ones because of what the four share. The tokens hold interned symbols, the untyped tree holds
9//! tokens, the typed tree holds the untyped tree's spans, and none of them owns the table it is
10//! reading, so one [`Session`] has to outlive all of them and there has to be one place that
11//! holds it.
12
13use std::path::Path;
14
15use rucc_diag::{Diagnostic, Severity, Span};
16use rucc_lex::{Convert, Keywords, PpToken, convert};
17use rucc_sema::{Checker, Context as CheckContext};
18use rucc_session::{EmitKind, FileSystem, Options, Session};
19
20use crate::preprocess::render;
21
22/// What compiling one file produced.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Compiled {
25    /// The text to write, empty when there was nothing to write or the compilation failed.
26    pub text: String,
27    /// The diagnostics, already rendered, one per element, in the order they were reported.
28    pub messages: Vec<String>,
29    /// How many of them were errors.
30    pub errors: u32,
31}
32
33impl Compiled {
34    /// Whether anything went wrong badly enough that the output should not be used.
35    #[must_use]
36    pub fn failed(&self) -> bool {
37        self.errors > 0
38    }
39}
40
41/// Compiles one file as far as `opts.emit` asks for and renders the result.
42///
43/// `name` is the path as the user wrote it, which is the name every diagnostic about the file
44/// uses. [`EmitKind::Tast`] and [`EmitKind::Ir`] produce text today. Every later kind runs the
45/// same front end and gives back nothing, so that a file with a mistake in it is reported the
46/// same way whichever of them was asked for, rather than compiling silently until the part
47/// that is written notices.
48///
49/// The checking is skipped when the parse reported an error. The two poisoning rules mean a
50/// diagnosed expression produces no further complaints, but a declaration the parser had to skip
51/// past leaves no declaration behind at all, and every later use of that name would be reported
52/// as undeclared. One mistake is worth one message.
53#[must_use]
54pub fn compile(opts: &Options, name: &str, fs: &dyn FileSystem) -> Compiled {
55    let mut sess = Session::new(opts.clone());
56    // Before anything else interns a name. The keyword symbols have to be one unbroken run for
57    // a lookup to be a subtraction, and the preprocessor interns every identifier it reads, so
58    // building this after the expansion would mean building it after `char` had been seen.
59    let keywords = Keywords::new(&mut sess.interner, opts.std, opts.gnu_extensions);
60    let mut diagnostics: Vec<Diagnostic> = Vec::new();
61
62    let bytes = match fs.read(Path::new(name)) {
63        Ok(bytes) => bytes,
64        Err(e) => return failure(format!("{name}: {e}")),
65    };
66    let Ok(file) = sess.sources.add_shared(name, bytes, None) else {
67        return failure(format!("{name}: the source map has no room left for this file"));
68    };
69
70    // Phases 1 to 4. The expanded stream is turned into pp-tokens straight away, because the
71    // include context borrows the source map that rendering a diagnostic reads and the borrow
72    // has to end before anything is rendered.
73    let mut pp = rucc_pp::Preprocessor::new();
74    let predef = rucc_pp::Predef::for_options(opts);
75    let expanded: Vec<PpToken> = {
76        let mut cx = rucc_pp::Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
77        if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
78            return failure(format!("{name}: the source map has no room for the built in macros"));
79        }
80        pp.run(file, &mut cx).iter().map(|token| token.to_pp()).collect()
81    };
82    diagnostics.extend(pp.take_diagnostics());
83
84    // Phase 7, which is where a spelling becomes a keyword and a preprocessing number becomes
85    // a constant of a type.
86    let cx = Convert {
87        keywords: &keywords,
88        interner: &sess.interner,
89        target: &sess.target,
90        std: opts.std,
91        pedantic: opts.pedantic,
92    };
93    let (tokens, complaints) = convert(&expanded, &cx);
94    diagnostics.extend(complaints);
95
96    let parsed = rucc_parse::parse(
97        &tokens,
98        rucc_parse::Context {
99            interner: &sess.interner,
100            std: opts.std,
101            gnu: opts.gnu_extensions,
102            pedantic: opts.pedantic,
103            error_limit: opts.error_limit as usize,
104        },
105    );
106    let parse_failed = parsed.diagnostics.iter().any(|d| d.severity.is_fatal());
107    diagnostics.extend(parsed.diagnostics);
108
109    let mut text = String::new();
110    if !parse_failed {
111        let mut checker = Checker::new(
112            &parsed.ast,
113            CheckContext {
114                names: &sess.interner,
115                target: &sess.target,
116                std: opts.std,
117                gnu: opts.gnu_extensions,
118                pedantic: opts.pedantic,
119                error_limit: opts.error_limit as usize,
120            },
121        );
122        checker.check_unit();
123        let checked = checker.finish();
124        if !checked.failed() {
125            match opts.emit {
126                EmitKind::Tast => {
127                    text = rucc_sema::print(&checked.tast, &checked.types, &sess.interner);
128                }
129                EmitKind::Ir => {
130                    let lowered = rucc_lower::lower(
131                        name,
132                        rucc_lower::Context {
133                            tast: &checked.tast,
134                            types: &checked.types,
135                            target: &sess.target,
136                            names: &mut sess.interner,
137                        },
138                    );
139                    // The walk reports what it cannot build, and what it did build is printed
140                    // anyway: a file with one construct missing from it is more use to read
141                    // than nothing at all, and the errors are what stop it being compiled.
142                    let failed = lowered.diagnostics.iter().any(|d| d.severity.is_fatal());
143                    if !failed {
144                        // The verifier runs on everything the walk builds, always. It is the
145                        // one check that a bug in the walk cannot talk its way past, and a
146                        // wrong instruction found here costs a message rather than an hour
147                        // in front of a debugger over the assembly it turned into.
148                        if let Err(errors) = rucc_ir::verify(&lowered.module, &sess.interner) {
149                            for error in errors {
150                                diagnostics.push(internal(&format!("invalid IR, {error}")));
151                            }
152                        } else {
153                            text = rucc_ir::print(&lowered.module, &sess.interner);
154                        }
155                    }
156                    diagnostics.extend(lowered.diagnostics);
157                }
158                _ => {}
159            }
160        }
161        diagnostics.extend(checked.diagnostics);
162    }
163
164    let mut messages = Vec::with_capacity(diagnostics.len());
165    let mut errors = 0;
166    for diag in &diagnostics {
167        if diag.severity.is_fatal()
168            || (diag.severity == Severity::Warning && opts.warnings_are_errors)
169        {
170            errors += 1;
171        }
172        messages.push(render(diag, &sess.sources, opts.warnings_are_errors));
173    }
174    if errors > 0 {
175        // A tree built from a file that did not compile is not a tree anything should read.
176        text.clear();
177    }
178    Compiled { text, messages, errors }
179}
180
181/// A diagnostic about this compiler rather than about the program it was given.
182fn internal(message: &str) -> Diagnostic {
183    Diagnostic::error(format!("internal error: {message}"), Span::DUMMY)
184        .with_code("E0652")
185        .note("this is a bug in rucc rather than in the program, please report it", Span::DUMMY)
186}
187
188/// A result that is nothing but one message, for the failures that happen before there is
189/// anything to compile.
190fn failure(message: String) -> Compiled {
191    Compiled { text: String::new(), messages: vec![format!("rucc: error: {message}")], errors: 1 }
192}
193
194#[cfg(test)]
195mod tests {
196    use rucc_session::{MemoryFileSystem, Std};
197    use rucc_target::Triple;
198
199    use super::*;
200
201    fn options() -> Options {
202        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
203        opts.emit = EmitKind::Tast;
204        opts
205    }
206
207    fn run(opts: &Options, source: &str) -> Compiled {
208        let mut fs = MemoryFileSystem::new();
209        fs.insert("/main.c", source.to_owned().into_bytes());
210        compile(opts, "/main.c", &fs)
211    }
212
213    /// The typed tree of `source`, insisting that it compiled cleanly.
214    fn tast(source: &str) -> String {
215        let result = run(&options(), source);
216        assert_eq!(result.messages, Vec::<String>::new(), "expected this to compile:\n{source}");
217        result.text
218    }
219
220    #[test]
221    fn a_file_that_is_not_there_says_so_and_produces_nothing() {
222        let fs = MemoryFileSystem::new();
223        let result = compile(&options(), "/nope.c", &fs);
224        assert!(result.failed());
225        assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
226        assert!(result.text.is_empty());
227    }
228
229    #[test]
230    fn an_object_comes_out_with_its_type_its_linkage_and_how_much_of_a_definition_it_is() {
231        let text = tast("int x = 1;\n");
232        let expected = "\
233decl #0 x : int object external static defined
234  init
235    +0
236      const 1 : int
237";
238        assert_eq!(text, expected);
239    }
240
241    #[test]
242    fn the_macros_are_expanded_before_anything_is_parsed() {
243        // The whole pipeline in one line. The bound came out of a macro, so it was expanded,
244        // converted from a preprocessing number to a constant of a type, parsed as an
245        // expression, and folded to the number the array type carries.
246        let text = tast("#define N 2\nint a[N];\n");
247        assert!(text.starts_with("decl #0 a : int [2] object external static tentative"), "{text}");
248    }
249
250    #[test]
251    fn every_conversion_the_language_performs_is_a_node_in_the_output() {
252        // The point of a typed tree. The source has one operator and the output has the
253        // widening that operator asked for, spelled out, so that nothing downstream has to
254        // work out the conversion rules a second time.
255        let text = tast("long f(int a, long b) { return a + b; }\n");
256        assert!(text.contains("convert arithmetic"), "{text}");
257    }
258
259    #[test]
260    fn a_mistake_in_each_phase_reaches_the_caller_and_writes_no_tree() {
261        for source in [
262            "#error stop\n",
263            "int f(void) { return 1 + ; }\n",
264            "int f(void) { return undeclared; }\n",
265        ] {
266            let result = run(&options(), source);
267            assert!(result.failed(), "expected this to fail:\n{source}");
268            assert!(result.text.is_empty(), "a file that did not compile wrote a tree:\n{source}");
269        }
270    }
271
272    #[test]
273    fn one_undeclared_name_is_one_message_and_not_one_per_use() {
274        // The poisoning rule from `spec/06-lexer-and-parser.md` section 6.8, seen from the
275        // outside. Three uses of a name that was never declared, and the operators over them
276        // say nothing at all.
277        let result = run(&options(), "int f(void) { return nope + nope * nope; }\n");
278        assert_eq!(result.errors, 1, "{:?}", result.messages);
279    }
280
281    #[test]
282    fn a_declaration_the_parser_skipped_does_not_become_an_undeclared_name_as_well() {
283        // The reason the checking is skipped after a failed parse. The parser gave up on the
284        // first line and there is no `x` in the tree, so a checker run over it would report
285        // every use of `x` below as undeclared, which is a second message about one mistake.
286        let result = run(&options(), "int x = ;\nint f(void) { return x; }\n");
287        assert_eq!(result.errors, 1, "{:?}", result.messages);
288    }
289
290    #[test]
291    fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
292        let source = "int f(void) { char c = 300; return c; }\n";
293        let plain = run(&options(), source);
294        assert_eq!(plain.errors, 0, "{:?}", plain.messages);
295        assert_eq!(plain.messages.len(), 1, "expected a warning about the narrowed constant");
296        assert!(!plain.text.is_empty(), "a warning is not a reason to write nothing");
297
298        let mut opts = options();
299        opts.warnings_are_errors = true;
300        let strict = run(&opts, source);
301        assert!(strict.failed());
302        assert!(strict.text.is_empty(), "and under -Werror it is a reason to write nothing");
303        for message in &strict.messages {
304            assert!(!message.contains("warning:"), "{message}");
305        }
306    }
307
308    #[test]
309    fn the_dialect_reaches_the_keywords_and_the_checking() {
310        // `typeof` is C23's and GNU's, so the same source is a declaration under one dialect
311        // and a mistake under the other, which is the keyword table being built per dialect.
312        let source = "typeof(1) x;\n";
313        let mut opts = options();
314        opts.std = Std::C23;
315        opts.gnu_extensions = false;
316        assert!(!run(&opts, source).failed(), "{:?}", run(&opts, source).messages);
317
318        opts.std = Std::C17;
319        assert!(run(&opts, source).failed());
320    }
321
322    #[test]
323    fn asking_for_a_kind_that_is_not_written_yet_runs_the_front_end_and_writes_nothing() {
324        let mut opts = options();
325        opts.emit = EmitKind::MirFinal;
326        let result = run(&opts, "int x = 1;\n");
327        assert!(!result.failed(), "{:?}", result.messages);
328        assert!(result.text.is_empty());
329        // And it still finds what the checking finds, so a later kind on a broken file is not
330        // a silent success.
331        assert!(run(&opts, "int f(void) { return undeclared; }\n").failed());
332    }
333
334    /// The IR of `source`, insisting that it compiled cleanly.
335    fn ir(source: &str) -> String {
336        let mut opts = options();
337        opts.emit = EmitKind::Ir;
338        let result = run(&opts, source);
339        assert_eq!(result.messages, Vec::<String>::new(), "expected this to compile:\n{source}");
340        result.text
341    }
342
343    /// The body of the one function in `source`, which is what most of these are about.
344    fn body(source: &str) -> String {
345        let text = ir(source);
346        let (_, rest) = text.split_once("{\n").expect("a function definition");
347        let (body, _) = rest.rsplit_once("}\n").expect("a function definition");
348        body.to_owned()
349    }
350
351    #[test]
352    fn an_object_becomes_a_global_with_an_image_and_a_function_becomes_a_func() {
353        let text = ir("int x = 7;\nint add(int a, int b) { return a + b; }\n");
354        assert!(text.contains("global @x : i32 = 7, align 4, linkage(external)\n"), "{text}");
355        let expected = "\
356func @add(i32, i32) -> i32, linkage(external) {
357block0(%0: i32, %1: i32):
358    %2 = add.nsw %0, %1
359    return %2
360}
361";
362        assert!(text.contains(expected), "{text}");
363    }
364
365    #[test]
366    fn a_local_nothing_takes_the_address_of_is_a_value_and_never_a_stack_slot() {
367        let text = body("int f(int n) { int a = n + 1; int b = a * 2; return a + b; }\n");
368        assert!(!text.contains("alloca"), "{text}");
369        assert!(!text.contains("load"), "{text}");
370        assert!(!text.contains("store"), "{text}");
371    }
372
373    #[test]
374    fn a_local_whose_address_is_taken_gets_a_slot_in_the_entry_block() {
375        let text = body("int g(int *);\nint f(void) { int a = 1; return g(&a); }\n");
376        let expected = "\
377block0:
378    %0 = alloca, size 4, align 4
379    %1 = iconst.i32 1
380    store %1 -> %0, align 4
381    %2 = call @g(%0) : (ptr) -> i32
382    return %2
383";
384        assert_eq!(text, expected);
385    }
386
387    #[test]
388    fn a_loop_carries_what_it_changes_as_block_parameters() {
389        // The whole point of building SSA during the walk rather than after it: `i` and
390        // `total` are values that arrive on an edge, and neither has ever been in memory.
391        let text = body(
392            "int f(int n) {\n  int total = 0;\n  for (int i = 0; i < n; i++) total += i;\n  \
393             return total;\n}\n",
394        );
395        assert!(!text.contains("alloca"), "{text}");
396        assert!(text.contains("block1(%3: i32, %4: i32):"), "{text}");
397        assert!(text.contains("jump block1("), "{text}");
398    }
399
400    #[test]
401    fn a_comparison_used_as_a_condition_is_not_widened_and_narrowed_again() {
402        let text = body("int f(int a, int b) { if (a < b) return 1; return 0; }\n");
403        assert!(text.contains("icmp slt %0, %1"), "{text}");
404        assert!(!text.contains("zext"), "{text}");
405    }
406
407    #[test]
408    fn the_right_side_of_a_short_circuit_is_in_a_block_of_its_own() {
409        let text = body("int f(int a, int b) { return a && b; }\n");
410        let expected = "\
411block0(%0: i32, %1: i32):
412    %2 = iconst.i32 0
413    %3 = icmp ne %0, %2
414    %4 = iconst.i1 0
415    br_if %3, block1, block2(%4)
416
417block1:
418    %5 = iconst.i32 0
419    %6 = icmp ne %1, %5
420    jump block2(%6)
421
422block2(%7: i1):
423    %8 = zext.i32 %7
424    return %8
425";
426        assert_eq!(text, expected);
427    }
428
429    #[test]
430    fn code_after_a_return_is_not_built_and_does_not_leave_an_empty_block_behind() {
431        let text = body("int f(int a) { if (a) return 1; else return 2; return 3; }\n");
432        // Three blocks, the test and the two arms. The join the `return 3` would need is
433        // never created, because a block nothing branches to is not a block.
434        assert!(!text.contains("block3"), "{text}");
435        assert!(!text.contains("iconst.i32 3"), "{text}");
436    }
437
438    #[test]
439    fn falling_off_the_end_returns_zero_from_main_and_nothing_from_a_void_function() {
440        assert!(body("int main(void) { }\n").contains("iconst.i32 0\n    return"));
441        assert_eq!(body("void f(void) { }\n"), "block0:\n    return\n");
442        assert!(body("int f(void) { }\n").contains("unreachable"));
443    }
444
445    #[test]
446    fn a_structure_is_copied_rather_than_held_in_a_value() {
447        let text = body(
448            "struct point { int x, y; };\n\
449             int f(void) { struct point p = { 1, 2 }; struct point q = p; return q.x; }\n",
450        );
451        assert!(text.contains("memcpy"), "{text}");
452    }
453
454    #[test]
455    fn an_initializer_that_leaves_part_of_an_object_unwritten_zeroes_it_first() {
456        let text = body("int f(void) { int a[4] = { 1 }; return a[3]; }\n");
457        assert!(text.contains("memset"), "{text}");
458    }
459
460    #[test]
461    fn a_switch_is_one_branch_and_a_case_that_falls_through_carries_what_it_wrote() {
462        let text = body(
463            "int f(int x) { int r = 0; switch (x) { case 1: r = 1; case 2: r += 2; break; \
464             default: r = 4; } return r; }\n",
465        );
466        let expected = "\
467block0(%0: i32):
468    %1 = iconst.i32 0
469    switch %0, block1, [1 => block2, 2 => block3(%1)]
470
471block1:
472    %2 = iconst.i32 4
473    jump block4(%2)
474
475block2:
476    %3 = iconst.i32 1
477    jump block3(%3)
478
479block3(%4: i32):
480    %5 = iconst.i32 2
481    %6 = add.nsw %4, %5
482    jump block4(%6)
483
484block4(%7: i32):
485    return %7
486";
487        assert_eq!(text, expected);
488    }
489
490    #[test]
491    fn a_case_range_is_tested_for_rather_than_put_in_the_table() {
492        // GNU's `case 1 ... 9`. Nine table entries would be nine here and four billion for the
493        // range a program is allowed to write, so it is a subtraction and one unsigned compare.
494        let text = body("int f(int x) { switch (x) { case 1 ... 9: return 1; } return 0; }\n");
495        assert!(text.contains("%2 = sub %0, %1"), "{text}");
496        assert!(text.contains("icmp ule"), "{text}");
497        assert!(!text.contains("switch"), "{text}");
498    }
499
500    #[test]
501    fn break_leaves_the_switch_and_continue_leaves_the_loop_around_it() {
502        let text = body(
503            "int f(int n) { int t = 0; for (int i = 0; i < n; i++) { switch (i) { \
504             case 0: continue; case 1: break; default: t += i; } t++; } return t; }\n",
505        );
506        // The `continue` goes to the step and the `break` goes to the `t++` after the switch,
507        // which is also where the default falls out to.
508        assert!(text.contains("switch %3, block4, [0 => block5, 1 => block6]"), "{text}");
509        assert!(text.contains("block5:\n    jump block7("), "{text}");
510        assert!(text.contains("block6:\n    jump block8("), "{text}");
511    }
512
513    #[test]
514    fn a_switch_with_nothing_to_branch_on_still_runs_what_comes_after_it() {
515        assert_eq!(body("void f(int x) { switch (x) { } }\n"), "block0(%0: i32):\n    return\n");
516    }
517
518    #[test]
519    fn a_label_control_cannot_fall_into_is_reported_rather_than_dropped() {
520        let mut opts = options();
521        opts.emit = EmitKind::Ir;
522        // A branch into the middle of a loop that nothing else reaches, once through a `switch`
523        // and once through a `goto`. The walk builds a loop from the top, so lowering either of
524        // these without the edge into the body would be a miscompile.
525        for source in [
526            "int f(int x, int n) { switch (x) { case 1: break; while (n) { case 2: n--; } } \
527             return n; }\n",
528            "int f(int x, int n) { goto in; while (n) { in: n--; } return n; }\n",
529        ] {
530            let result = run(&opts, source);
531            assert!(result.failed(), "expected this to be reported:\n{source}");
532            assert!(
533                result.messages.iter().any(|m| m.contains("a label control cannot fall into")),
534                "{:?}",
535                result.messages
536            );
537        }
538    }
539
540    #[test]
541    fn a_goto_is_a_jump_to_the_block_the_label_starts() {
542        let text = body("int f(int x) { int r = 0; if (x) goto out; r = 1; out: return r; }\n");
543        // Both edges into `out` carry what `r` holds on the way, and neither is a stack slot.
544        assert!(!text.contains("alloca"), "{text}");
545        assert!(text.contains("block3(%4: i32):\n    return %4"), "{text}");
546        assert_eq!(text.matches("jump block3(").count(), 2, "{text}");
547    }
548
549    #[test]
550    fn a_backward_goto_is_a_loop_and_carries_what_it_changes() {
551        let text =
552            body("int f(int n) { int i = 0; again: if (i < n) { i++; goto again; } return i; }\n");
553        assert!(!text.contains("alloca"), "{text}");
554        assert!(text.contains("block1(%2: i32):"), "{text}");
555        assert!(text.contains("jump block1(%5)"), "{text}");
556    }
557
558    #[test]
559    fn a_label_nothing_reaches_is_taken_out_rather_than_left_for_the_verifier() {
560        // A block nothing branches to is not a legal function, and which labels are dead is not
561        // known until the last statement has been walked, since the `goto` is allowed to be it.
562        assert_eq!(
563            body("int f(int x) { return x; spare: return 0; }\n"),
564            "block0(%0: i32):\n    return %0\n"
565        );
566    }
567
568    #[test]
569    fn what_the_walk_cannot_build_yet_is_reported_rather_than_mislowered() {
570        let mut opts = options();
571        opts.emit = EmitKind::Ir;
572        for source in [
573            "int f(int n) { int a[n]; a[0] = 1; return a[0]; }\n",
574            "int f(int x) { void *p = &&out; goto *p; out: return x; }\n",
575            "struct s { int a : 3; };\nint f(struct s *p) { return p->a; }\n",
576            "struct s { int a[4]; };\nint f(struct s v);\nint g(struct s v) { return f(v); }\n",
577        ] {
578            let result = run(&opts, source);
579            assert!(result.failed(), "expected this to be reported:\n{source}");
580            assert!(
581                result.messages.iter().any(|m| m.contains("not supported yet")),
582                "{:?}",
583                result.messages
584            );
585        }
586    }
587
588    #[test]
589    fn the_printed_ir_reads_back_as_the_same_module() {
590        // The M2 exit criterion: the text is the module and nothing about it is lost by
591        // writing it down. Anything the printer invents or the parser drops shows up here.
592        let text = ir("\
593struct point { int x, y; };
594static const char greeting[] = \"hi\";
595int table[4] = { 1, 2, 3 };
596int puts(const char *);
597double half(double x) { return x / 2.0; }
598int f(int n) {
599  int total = 0;
600  for (int i = 0; i < n; i++) {
601    if (i == 3) continue;
602    total += table[i];
603  }
604  switch (n) {
605    case 0: total = 1;
606    case 1: total++; break;
607    default: total = -total;
608  }
609  struct point p = { total, 1 };
610  int *q = &p.y;
611  puts(greeting);
612  return p.x + *q;
613}
614");
615        let mut names = rucc_base::Interner::new();
616        let module = rucc_ir::parse(&text, &mut names).expect("the printer writes what it reads");
617        assert_eq!(rucc_ir::print(&module, &names), text);
618    }
619}