calyx_frontend/
workspace.rs

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