Skip to main content

marlin_spade/
lib.rs

1// Copyright (C) 2024 Ethan Uppal.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License,
4// v. 2.0. If a copy of the MPL was not distributed with this file, You can
5// obtain one at https://mozilla.org/MPL/2.0/.
6
7//! Spade integration for Marlin.
8
9use std::{env::current_dir, ffi::OsString, fs, process::Command};
10
11use camino::{Utf8Path, Utf8PathBuf};
12use marlin_verilator::{
13    AsVerilatedModel, VerilatedModelConfig, VerilatorRuntime,
14    VerilatorRuntimeOptions, eprintln_nocapture,
15};
16use owo_colors::OwoColorize;
17use snafu::{OptionExt, ResultExt, Whatever, whatever};
18
19#[doc(hidden)]
20pub mod __reexports {
21    pub use libloading;
22    pub use marlin_verilator as verilator;
23}
24
25pub mod prelude {
26    pub use crate as spade;
27    pub use crate::{SpadeRuntime, SpadeRuntimeOptions};
28    pub use marlin_spade_macro::spade;
29    pub use marlin_verilator::{AsDynamicVerilatedModel, AsVerilatedModel};
30}
31
32const SWIM_TOML: &str = "swim.toml";
33
34fn search_for_swim_toml(mut start: Utf8PathBuf) -> Option<Utf8PathBuf> {
35    while start.parent().is_some() {
36        if start.join(SWIM_TOML).is_file() {
37            return Some(start.join(SWIM_TOML));
38        }
39        start.pop();
40    }
41    None
42}
43
44/// Optional configuration for creating a [`SpadeRuntime`]. Usually, you can
45/// just use [`SpadeRuntimeOptions::default()`].
46pub struct SpadeRuntimeOptions {
47    /// The name of the `swim` executable, interpreted in some way by the
48    /// OS/shell.
49    pub swim_executable: OsString,
50
51    /// Whether `swim build` should be automatically called. This switch is
52    /// useful to disable when, for example, another tool has already
53    /// called `swim build`.
54    pub call_swim_build: bool,
55
56    /// See [`VerilatorRuntimeOptions`].
57    pub verilator_options: VerilatorRuntimeOptions,
58}
59
60impl Default for SpadeRuntimeOptions {
61    fn default() -> Self {
62        Self {
63            swim_executable: "swim".into(),
64            call_swim_build: false,
65            verilator_options: VerilatorRuntimeOptions::default(),
66        }
67    }
68}
69
70impl SpadeRuntimeOptions {
71    /// The same as the [`Default`] implementation except that the log crate is
72    /// used.
73    pub fn default_logging() -> Self {
74        Self {
75            verilator_options: VerilatorRuntimeOptions::default_logging(),
76            ..Default::default()
77        }
78    }
79}
80
81/// Optional configuration for creating an [`AsVerilatedModel`]. Usually, you
82/// can just use [`SpadeModelConfig::default()`].
83#[derive(Default)]
84pub struct SpadeModelConfig {
85    /// See [`VerilatedModelConfig`].
86    pub verilator_config: VerilatedModelConfig,
87}
88
89/// Runtime for Spade code.
90pub struct SpadeRuntime {
91    verilator_runtime: VerilatorRuntime,
92}
93
94impl SpadeRuntime {
95    /// Creates a new runtime for instantiating Spade units as Rust objects.
96    /// Does NOT call `swim build` by defaul because `swim build` is not
97    /// thread safe. You can enable this with [`SwimRuntimeOptions`] or just
98    /// run it beforehand.
99    pub fn new(options: SpadeRuntimeOptions) -> Result<Self, Whatever> {
100        if options.verilator_options.log {
101            log::info!("Searching for swim project root");
102        }
103        let Some(swim_toml_path) = search_for_swim_toml(
104            current_dir()
105                .whatever_context("Failed to get current directory")?
106                .try_into()
107                .whatever_context(
108                    "Failed to convert current directory to UTF-8",
109                )?,
110        ) else {
111            whatever!(
112                "Failed to find {SWIM_TOML} searching from current directory"
113            );
114        };
115        let mut swim_project_path = swim_toml_path.clone();
116        swim_project_path.pop();
117
118        let swim_toml_contents = fs::read_to_string(&swim_toml_path)
119            .whatever_context(format!(
120                "Failed to read contents of {SWIM_TOML} at {swim_toml_path}"
121            ))?;
122        let swim_toml: toml::Value = toml::from_str(&swim_toml_contents)
123            .whatever_context(format!(
124                "Failed to parse {SWIM_TOML} as a valid TOML file"
125            ))?;
126
127        if options.call_swim_build {
128            if options.verilator_options.log {
129                log::info!("Invoking `swim build` (this may take a while)");
130            }
131
132            let swim_project_name = swim_toml
133                .get("name")
134                .and_then(|name| name.as_str())
135                .whatever_context(format!(
136                    "{SWIM_TOML} missing top-level `name` field"
137                ))?;
138
139            eprintln_nocapture!(
140                "{} {swim_project_name} ({swim_project_path})",
141                "   Compiling".bold().green()
142            )?;
143
144            let swim_output = Command::new(options.swim_executable)
145                .arg("build")
146                .current_dir(&swim_project_path)
147                .output()
148                .whatever_context("Invocation of swim failed")?;
149
150            if !swim_output.status.success() {
151                whatever!(
152                    "Invocation of swim failed with nonzero exit code {}\n\n--- STDOUT ---\n{}\n\n--- STDERR ---\n{}",
153                    swim_output.status,
154                    String::from_utf8(swim_output.stdout).unwrap_or_default(),
155                    String::from_utf8(swim_output.stderr).unwrap_or_default()
156                );
157            }
158        }
159
160        let spade_sv_path = swim_project_path.join("build/spade.sv");
161
162        let extra_verilog = swim_toml.get("verilog").map(|verilog| {
163            (
164                verilog
165                    .get("sources")
166                    .and_then(|sources| sources.as_array())
167                    .map(|sources| {
168                        let mut result = vec![];
169                        for source in
170                            sources.iter().flat_map(|source| source.as_str())
171                        {
172                            if let Ok(paths) = glob::glob(
173                                swim_project_path.join(source).as_str(),
174                            ) {
175                                for path in paths.flatten() {
176                                    if let Ok(path) =
177                                        Utf8PathBuf::try_from(path)
178                                    {
179                                        result.push(path);
180                                    }
181                                }
182                            }
183                        }
184                        result
185                    }),
186                verilog
187                    .get("include")
188                    .and_then(|include| include.as_array())
189                    .map(|sources| {
190                        sources
191                            .iter()
192                            .flat_map(|source| source.as_str())
193                            .flat_map(|source| {
194                                Utf8Path::new(source).canonicalize_utf8()
195                            })
196                            .collect::<Vec<_>>()
197                    }),
198            )
199        });
200
201        let mut source_files = vec![spade_sv_path.as_path()];
202        let mut include_files = vec![];
203
204        if let Some(extra_verilog) = &extra_verilog {
205            if let Some(sources) = &extra_verilog.0 {
206                source_files
207                    .extend(sources.iter().map(|source| source.as_path()));
208            }
209            if let Some(include) = &extra_verilog.1 {
210                include_files.extend(
211                    include.iter().map(|directory| directory.as_path()),
212                );
213            }
214        }
215
216        Ok(Self {
217            verilator_runtime: VerilatorRuntime::new(
218                // https://discord.com/channels/962274366043873301/962296357018828822/1332274022280466503
219                &swim_project_path.join("build/thirdparty/marlin"),
220                &source_files,
221                &include_files,
222                [],
223                options.verilator_options,
224            )?,
225        })
226    }
227
228    /// Instantiates a new Spade unit. This function simply wraps
229    /// [`VerilatorRuntime::create_model_simple`].
230    pub fn create_model_simple<'ctx, M: AsVerilatedModel<'ctx>>(
231        &'ctx self,
232    ) -> Result<M, Whatever> {
233        self.verilator_runtime.create_model_simple()
234    }
235
236    /// Instantiates a new Spade unit. This function simply wraps
237    /// [`VerilatorRuntime::create_model`].
238    pub fn create_model<'ctx, M: AsVerilatedModel<'ctx>>(
239        &'ctx self,
240        config: SpadeModelConfig,
241    ) -> Result<M, Whatever> {
242        self.verilator_runtime
243            .create_model(&config.verilator_config)
244    }
245}