calyx_frontend/
workspace.rs

1use super::{
2    ast::{ComponentDef, NamespaceDef},
3    parser,
4};
5use crate::{LibrarySignatures, source_info::SourceInfoTable};
6use calyx_utils::{CalyxResult, Error, WithPos};
7use itertools::Itertools;
8use std::{
9    collections::HashSet,
10    path::{Path, PathBuf},
11};
12
13/// String representing the basic compilation primitives that need to be present
14/// to support compilation.
15const COMPILE_LIB: &str = include_str!("../resources/compile.futil");
16
17/// A Workspace represents all Calyx files transitively discovered while trying to compile a
18/// top-level file.
19///
20/// # Example
21/// When parsing a file `foo.futil`:
22/// ```text
23/// import "core.futil";
24///
25/// component main() -> () { ... }
26/// ```
27///
28/// The workspace gets the absolute path for `core.futil` and adds `main` to the set of defined
29/// components. `core.futil` is searched *both* relative to the current file and the library path.
30/// Next `core.futil` is parsed:
31/// ```
32/// extern "core.sv" {
33///     primitive std_add[width](left: width, right: width) -> (out: width);
34/// }
35/// ```
36/// The workspace adds `std_add` to the currently defined primitives and looks for `core.sv` in a
37/// relative path to this file. It *does not* look for `core.sv` on the library path.
38///
39/// Finally, since `core.futil` does not `import` any file, the parsing process is completed.
40#[derive(Default)]
41pub struct Workspace {
42    /// List of component definitions that need to be compiled.
43    pub components: Vec<ComponentDef>,
44    /// List of component definitions that should be used as declarations and
45    /// not compiled. This is used when the compiler is invoked with File
46    /// compilation mode.
47    pub declarations: Vec<ComponentDef>,
48    /// Absolute path to extern definitions and primitives defined by them.
49    pub lib: LibrarySignatures,
50    /// Original import statements present in the top-level file.
51    pub original_imports: Vec<String>,
52    /// Optional opaque metadata attached to the top-level file.
53    pub metadata: Option<String>,
54    /// Optional source info table attached to the top-level file
55    pub source_info_table: Option<SourceInfoTable>,
56}
57
58impl Workspace {
59    /// Returns the absolute location to an imported file.
60    ///
61    /// An import path is first resolved as an absolute or
62    /// relative(-to-`parent`) path, and if no file exists at either such
63    /// extended path exists, it assumed to be under the library path
64    /// `lib_paths`.
65    fn canonicalize_import<S>(
66        import: S,
67        parent: &Path,
68        lib_paths: &[PathBuf],
69    ) -> CalyxResult<PathBuf>
70    where
71        S: AsRef<Path> + Clone + WithPos,
72    {
73        let absolute_import = import.as_ref();
74        if absolute_import.is_absolute() && absolute_import.exists() {
75            return Ok(import.as_ref().to_path_buf());
76        }
77
78        let relative_import = parent.join(&import);
79        if relative_import.exists() {
80            return Ok(relative_import);
81        }
82
83        let library_imports: Vec<_> = lib_paths
84            .iter()
85            .filter_map(|lib_path| {
86                let library_import = lib_path.join(&import);
87                library_import.exists().then_some(library_import)
88            })
89            .collect();
90
91        match library_imports.len() {
92            0 => {
93                Err(Error::invalid_file(format!(
94                    "Import path `{}` found neither as an absolute path, nor in the parent ({}), nor in library path ({})",
95                    import.as_ref().to_string_lossy(),
96                    parent.to_string_lossy(),
97                    lib_paths.iter().map(|p| p.to_string_lossy()).format(", ")
98                ))
99                .with_pos(&import))
100            }
101            1 => Ok(library_imports.into_iter().next().unwrap()),
102            _ => {
103                Err(Error::misc(format!(
104                    "Import path `{}` found in multiple library paths ({})",
105                    import.as_ref().to_string_lossy(),
106                    library_imports
107                        .iter()
108                        .map(|p| p.to_string_lossy())
109                        .format(", ")
110                ))
111                .with_pos(&import))
112            }
113        }
114    }
115
116    // Get the absolute path to an extern. Extern can only exist on paths
117    // relative to the parent.
118    #[cfg(not(target_arch = "wasm32"))]
119    fn canonicalize_extern<S>(
120        extern_path: S,
121        parent: &Path,
122    ) -> CalyxResult<PathBuf>
123    where
124        S: AsRef<Path> + Clone + WithPos,
125    {
126        parent
127            .join(extern_path.clone())
128            .canonicalize()
129            .map_err(|_| {
130                Error::invalid_file(format!(
131                    "Extern path `{}` not found in parent directory ({})",
132                    extern_path.as_ref().to_string_lossy(),
133                    parent.to_string_lossy(),
134                ))
135                .with_pos(&extern_path)
136            })
137    }
138
139    /// Construct a new workspace using the `compile.futil` library which
140    /// contains the core primitives needed for compilation.
141    pub fn from_compile_lib() -> CalyxResult<Self> {
142        let mut ns = NamespaceDef::construct_from_str(COMPILE_LIB)?;
143        // No imports allowed
144        assert!(
145            ns.imports.is_empty(),
146            "core library should not contain any imports"
147        );
148        // No metadata allowed
149        assert!(
150            ns.metadata.is_none(),
151            "core library should not contain any metadata"
152        );
153        // Only inline externs are allowed
154        assert!(
155            ns.externs.len() == 1 && ns.externs[0].0.is_none(),
156            "core library should only contain inline externs"
157        );
158        let (_, externs) = ns.externs.pop().unwrap();
159        let mut lib = LibrarySignatures::default();
160        for ext in externs {
161            lib.add_inline_primitive(ext);
162        }
163        let ws = Workspace {
164            components: ns.components,
165            lib,
166            ..Default::default()
167        };
168        Ok(ws)
169    }
170
171    /// Construct a new workspace from an input stream representing a Calyx
172    /// program.
173    pub fn construct(
174        file: &Option<PathBuf>,
175        lib_paths: &[PathBuf],
176    ) -> CalyxResult<Self> {
177        Self::construct_with_all_deps::<false>(
178            file.iter().cloned().collect(),
179            lib_paths,
180        )
181    }
182
183    /// Construct the Workspace using the given [NamespaceDef] and ignore all
184    /// imported dependencies.
185    pub fn construct_shallow(
186        file: &Option<PathBuf>,
187        lib_paths: &[PathBuf],
188    ) -> CalyxResult<Self> {
189        Self::construct_with_all_deps::<true>(
190            file.iter().cloned().collect(),
191            lib_paths,
192        )
193    }
194
195    fn get_parent(p: &Path) -> PathBuf {
196        let maybe_parent = p.parent();
197        match maybe_parent {
198            None => PathBuf::from("."),
199            Some(path) => {
200                if path.to_string_lossy() == "" {
201                    PathBuf::from(".")
202                } else {
203                    PathBuf::from(path)
204                }
205            }
206        }
207    }
208
209    /// Merge the contents of a namespace into this workspace.
210    /// `is_source` identifies this namespace as a source file.
211    /// The output is a list of files that need to be parsed next and whether they are source files.
212    pub fn merge_namespace(
213        &mut self,
214        ns: NamespaceDef,
215        is_source: bool,
216        parent: &Path,
217        shallow: bool,
218        lib_paths: &[PathBuf],
219    ) -> CalyxResult<Vec<(PathBuf, bool)>> {
220        // Canonicalize the extern paths and add them
221        for (path, exts) in ns.externs {
222            match path {
223                Some(p) => {
224                    #[cfg(not(target_arch = "wasm32"))]
225                    let abs_path = Self::canonicalize_extern(p, parent)?;
226
227                    // For the WebAssembly target, we avoid depending on the filesystem to
228                    // canonicalize paths to imported files. (This canonicalization is not
229                    // necessary because imports for the WebAssembly target work differently
230                    // anyway.)
231                    #[cfg(target_arch = "wasm32")]
232                    let abs_path = p.into();
233
234                    let p = self.lib.add_extern(abs_path, exts);
235                    if is_source {
236                        p.set_source();
237                    }
238                }
239                None => {
240                    for ext in exts {
241                        let p = self.lib.add_inline_primitive(ext);
242                        if is_source {
243                            p.set_source();
244                        }
245                    }
246                }
247            }
248        }
249
250        // Add components defined by this namespace to either components or
251        // declarations
252        if !is_source && shallow {
253            self.declarations.extend(&mut ns.components.into_iter());
254        } else {
255            self.components.extend(&mut ns.components.into_iter());
256        }
257        // Return the canonical location of import paths
258        let deps = ns
259            .imports
260            .into_iter()
261            .map(|p| {
262                Self::canonicalize_import(p, parent, lib_paths)
263                    .map(|s| (s, false))
264            })
265            .collect::<CalyxResult<_>>()?;
266
267        Ok(deps)
268    }
269
270    /// Construct the Workspace using the given files and all their dependencies.
271    /// If SHALLOW is true, then parse imported components as declarations and not added to the workspace components.
272    /// If in doubt, set SHALLOW to false.
273    pub fn construct_with_all_deps<const SHALLOW: bool>(
274        mut files: Vec<PathBuf>,
275        lib_paths: &[PathBuf],
276    ) -> CalyxResult<Self> {
277        // Construct initial namespace. If `files` is empty, then we're reading from the standard input.
278        let first = files.pop();
279        let ns = NamespaceDef::construct(&first)?;
280        let parent_path = first
281            .as_ref()
282            .map(|p| Self::get_parent(p))
283            .unwrap_or_else(|| PathBuf::from("."));
284
285        // Set of current dependencies and whether they are considered source files.
286        let mut dependencies: Vec<(PathBuf, bool)> =
287            files.into_iter().map(|p| (p, true)).collect();
288        // Set of imports that have already been parsed once.
289        let mut already_imported: HashSet<PathBuf> = HashSet::new();
290
291        let mut ws = Workspace::default();
292
293        let abs_lib_paths: Vec<_> = lib_paths
294            .iter()
295            .map(|lib_path| {
296                lib_path.canonicalize().map_err(|err| {
297                    Error::invalid_file(format!(
298                        "Failed to canonicalize library path `{}`: {}",
299                        lib_path.to_string_lossy(),
300                        err
301                    ))
302                })
303            })
304            .collect::<CalyxResult<_>>()?;
305
306        // Add original imports to workspace
307        ws.original_imports =
308            ns.imports.iter().map(|imp| imp.to_string()).collect();
309
310        // TODO (griffin): Probably not a great idea to clone the metadata
311        // string but it works for now
312        ws.metadata = ns.metadata.clone();
313        ws.source_info_table = ns.source_info_table.clone();
314
315        // Merge the initial namespace
316        let parent_canonical = parent_path.canonicalize().map_err(|err| {
317            Error::invalid_file(format!(
318                "Failed to canonicalize parent path `{}`: {}",
319                parent_path.to_string_lossy(),
320                err
321            ))
322        })?;
323        let mut deps = ws.merge_namespace(
324            ns,
325            true,
326            &parent_canonical,
327            false,
328            &abs_lib_paths,
329        )?;
330        dependencies.append(&mut deps);
331
332        while let Some((p, source)) = dependencies.pop() {
333            if already_imported.contains(&p) {
334                continue;
335            }
336            let ns = parser::CalyxParser::parse_file(&p)?;
337            let parent = Self::get_parent(&p);
338
339            let mut deps = ws.merge_namespace(
340                ns,
341                source,
342                &parent,
343                SHALLOW,
344                &abs_lib_paths,
345            )?;
346            dependencies.append(&mut deps);
347
348            already_imported.insert(p);
349        }
350        Ok(ws)
351    }
352}