Skip to main content

device_driver_core/
lib.rs

1#![doc = include_str!(concat!("../", env!("CARGO_PKG_README")))]
2
3use std::fmt::Write;
4
5use clap::Parser;
6use device_driver_diagnostics::{Diagnostics, DynError, ResultExt};
7use itertools::Itertools;
8
9pub use device_driver_codegen::{RustCodegenOptions, Target as CodegenTarget};
10pub use device_driver_diagnostics::Metadata;
11pub use device_driver_mir::MirOptions;
12
13use crate::timings::Timings;
14
15mod timings;
16
17#[derive(Parser, Debug, Clone)]
18#[command(no_binary_name = true, bin_name = "")]
19pub struct CompileOptions {
20    #[command(flatten)]
21    pub general_options: GeneralOptions,
22    #[command(flatten)]
23    pub mir_options: MirOptions,
24    #[command(subcommand)]
25    pub target: CodegenTarget,
26}
27
28#[derive(Parser, Debug, Clone, Default)]
29#[command(no_binary_name = true)]
30pub struct GeneralOptions {
31    /// Improves reproducibility across versions
32    #[arg(long = "unstable-ui-test-mode", global = true)]
33    pub ui_test_mode: bool,
34    /// When enabled, a diagnostic is printed with information about the compiler performance.
35    /// Exact format of the diagnostic is unstable.
36    #[arg(long, global = true, require_equals = true, default_value = "off")]
37    pub timings: TimingsMode,
38}
39
40#[derive(clap::ValueEnum, Debug, Clone, Copy, Default)]
41pub enum TimingsMode {
42    #[default]
43    Off,
44    Show,
45    Verbose,
46}
47
48pub fn compile(source: &str, options: CompileOptions) -> Result<(String, Diagnostics), DynError> {
49    let mut timings = Timings::new(options.general_options.timings);
50    let mut diagnostics = Diagnostics::new();
51
52    let tokens = {
53        let _t = timings.start_lexer(source.len());
54        device_driver_lexer::lex(source)
55    };
56    let ast = {
57        let _t = timings.start_parser(tokens.len());
58        device_driver_parser::parse(&tokens, &mut diagnostics)
59    };
60    let (mir, mir_timings) = {
61        let _t = timings.start_mir();
62        device_driver_mir::lower_ast(&ast, &options.mir_options, &mut diagnostics)
63            .with_message(|| "could not lower AST to MIR")?
64    };
65    timings.set_mir_timings(mir_timings);
66    let lir = {
67        let _t = timings.start_lir();
68        device_driver_lir::lower_mir(mir).with_message(|| "could not lower MIR to LIR")?
69    };
70    let mut code = {
71        let _t = timings.start_codegen();
72        device_driver_codegen::codegen(&options.target, &lir, source)
73    };
74
75    if diagnostics.has_error() {
76        let _ = write!(code, "\n{}\n", options.target.create_error_message());
77    }
78
79    // TODO: Make formatting dependent on the target. Right now it's just Rust
80    let formatted_code = {
81        let _t = timings.start_formatting();
82        match format_code(&code) {
83            Ok(formatted_code) => formatted_code,
84            Err(e) => format!(
85                "{}\n\n{code}",
86                e.to_string().lines().map(|e| format!("// {e}")).join("\n")
87            ),
88        }
89    };
90
91    let preamble = options.target.to_comments(&format!(
92        "This code was generated using device-driver `{}` ({}),
93a tool distributed under {} by {}
94
95For more information about device-driver, visit the website: {}",
96        if options.general_options.ui_test_mode {
97            "xx.xx.xx"
98        } else {
99            env!("CARGO_PKG_VERSION")
100        },
101        if options.general_options.ui_test_mode {
102            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
103        } else {
104            env!("BUILDRS_GIT_SHA")
105        },
106        env!("CARGO_PKG_LICENSE"),
107        env!("CARGO_PKG_AUTHORS"),
108        env!("CARGO_PKG_HOMEPAGE"),
109    ));
110
111    let formatted_code = preamble + "\n\n" + &formatted_code;
112
113    if !matches!(options.general_options.timings, TimingsMode::Off) {
114        diagnostics.add(timings);
115    }
116
117    Ok((formatted_code, diagnostics))
118}
119
120#[cfg(feature = "gen-docs")]
121pub fn gen_docs(output_path: &std::path::Path) -> Result<(), DynError> {
122    std::fs::create_dir_all(output_path).with_message(|| {
123        format!(
124            "creating folder for gen-docs output at {}",
125            output_path.display()
126        )
127    })?;
128
129    let parser_folder = output_path.join("parser");
130    if !parser_folder.exists() {
131        std::fs::create_dir(&parser_folder).with_message(|| {
132            format!(
133                "creating folder for gen-docs parser output at {}",
134                parser_folder.display()
135            )
136        })?;
137    }
138    device_driver_parser::gen_docs::gen_docs(&parser_folder)
139        .with_message(|| "gen-docs for parser")?;
140
141    let mir_shapes_folder = output_path.join("mir-shapes");
142    if !mir_shapes_folder.exists() {
143        std::fs::create_dir(&mir_shapes_folder).with_message(|| {
144            format!(
145                "creating folder for gen-docs mir-shapes output at {}",
146                mir_shapes_folder.display()
147            )
148        })?;
149    }
150    device_driver_mir::gen_docs(&mir_shapes_folder).with_message(|| "gen-docs for mir shapes")?;
151
152    Ok(())
153}
154
155#[cfg(not(feature = "prettyplease"))]
156fn format_code(input: &str) -> Result<String, DynError> {
157    use std::io::{Read, Write};
158    use std::process::Stdio;
159
160    use device_driver_diagnostics::ResultExt;
161
162    let mut cmd = std::process::Command::new("rustfmt");
163
164    cmd.args(["--edition", "2024"])
165        .args(["--config", "newline_style=native"])
166        .args(["--color", "never"])
167        .stdin(Stdio::piped())
168        .stdout(Stdio::piped())
169        .stderr(Stdio::piped());
170
171    let mut child = cmd
172        .spawn()
173        .with_message(|| "error while spawning rustfmt")?;
174    let mut child_stdin = child.stdin.take().unwrap();
175    let mut child_stdout = child.stdout.take().unwrap();
176
177    // Write to stdin in a new thread, so that we can read from stdout on this
178    // thread. This keeps the child from blocking on writing to its stdout which
179    // might block us from writing to its stdin.
180    let output = std::thread::scope(|s| {
181        s.spawn(|| {
182            child_stdin
183                .write_all(input.as_bytes())
184                .with_message(|| "couldn't write input to rustfmt")?;
185            child_stdin
186                .flush()
187                .with_message(|| "couldn't flush input to rustfmt")?;
188            drop(child_stdin);
189            Result::<(), DynError>::Ok(())
190        });
191        let handle: std::thread::ScopedJoinHandle<'_, Result<Vec<u8>, DynError>> = s.spawn(|| {
192            let mut output = Vec::new();
193            child_stdout.read_to_end(&mut output).into_dyn_result()?;
194            Ok(output)
195        });
196
197        handle.join()
198    });
199
200    let status = child.wait().into_dyn_result()?;
201    if !status.success() {
202        return Err(DynError::new(format!(
203            "rustfmt exited unsuccessfully ({status}):\n{}",
204            child
205                .stderr
206                .map(|mut stderr| {
207                    let mut err = String::new();
208                    stderr.read_to_string(&mut err).unwrap();
209                    err
210                })
211                .unwrap_or_default()
212        )));
213    }
214
215    let output = match output {
216        Ok(output) => output,
217        Err(e) => std::panic::resume_unwind(e),
218    };
219
220    String::from_utf8(output?).into_dyn_result()
221}
222
223#[cfg(feature = "prettyplease")]
224fn format_code(input: &str) -> Result<String, syn::Error> {
225    Ok(prettyplease::unparse(&syn::parse_file(input)?))
226}