Skip to main content

seahorse_dev/core/preprocess/
mod.rs

1// TODO split so that we don't just have all the code in mod.rs
2use crate::core::{
3    clean::{ast as ca, clean},
4    parse::parse,
5    util::*,
6    CoreError,
7};
8use std::{
9    collections::HashMap,
10    ffi::OsStr,
11    fmt::{self, Display, Formatter},
12    fs::{read_dir, File},
13    io::Read,
14    path::PathBuf,
15};
16
17use super::compile::builtin::prelude::path_to_string;
18
19enum Error {
20    CouldNotAddModule,
21    CouldNotFind(ComboPath),
22    RelativeSeahorseImport,
23    PathOutsideRoot(ComboPath, usize),
24    Os,
25}
26
27impl Error {
28    fn core(&self) -> CoreError {
29        match self {
30            Self::CouldNotAddModule => CoreError::make_raw(
31                "could not add module",
32                "",
33            ),
34            Self::CouldNotFind(path) => CoreError::make_raw(
35                format!("could not find import at {}", path),
36                "",
37            ),
38            Self::RelativeSeahorseImport => CoreError::make_raw(
39                "attempted to import local Seahorse files",
40                "Help: `seahorse` is a builtin package, you should import it like a regular Python package:\n\n    from seahorse.prelude import *"
41            ),
42            Self::PathOutsideRoot(path, level) => CoreError::make_raw(
43                "relative import would go outside of source code root",
44                format!(
45                    "Hint: you can't go up {} directory levels from {}.",
46                    level, path
47                ),
48            ),
49            Self::Os => CoreError::make_raw(
50                "OS error",
51                "",
52            ),
53        }
54    }
55}
56
57#[derive(Clone, Debug)]
58pub struct ModuleRegistry {
59    pub tree: Tree<Module>,
60    pub origin: Vec<String>,
61    pub order: Vec<String>,
62}
63
64impl ModuleRegistry {
65    /// Get an absolute path. The registry search behavior is as such:
66    ///     1. Check if from+ext is a valid path (searches relative area first)
67    ///     2. For each `src` (specified by `self.order`), check if src+ext is a valid path.
68    ///
69    /// Returns None if a valid path could not be found.
70    pub fn get_abs_path(&self, from: &Vec<String>, ext: &Vec<String>) -> Option<Vec<String>> {
71        let mut path = from.clone();
72        path.append(&mut ext.clone());
73
74        if self.tree.get(&path).is_some() {
75            return Some(path);
76        }
77
78        for src in self.order.iter() {
79            let mut path = vec![src.clone()];
80            path.append(&mut ext.clone());
81
82            if self.tree.get(&path).is_some() {
83                return Some(path);
84            }
85        }
86
87        return None;
88    }
89}
90
91/// Load a module from path, parse and clean it.
92/// TODO unused so far, will be used when imports are really added
93fn load_module(path: PathBuf) -> CResult<Module> {
94    let mut input = File::open(path.clone())
95        .map_err(|_| CoreError::make_raw(format!("could not open file {}", path.display()), ""))?;
96    let mut py_src = String::new();
97    input
98        .read_to_string(&mut py_src)
99        .map_err(|_| CoreError::make_raw("I/O error", ""))?;
100
101    let parsed = parse(py_src.clone())?;
102    let module = clean(parsed, py_src)?;
103
104    return Ok(Module::Python(module));
105}
106
107fn from_os_string(os: &OsStr) -> String {
108    os.to_str().unwrap().to_string()
109}
110
111#[derive(Clone, Debug)]
112pub enum Module {
113    Python(ca::Module),
114    SeahorsePrelude,
115    SeahorsePyth,
116}
117
118/// A combined registry tree + filesystem path.
119///
120/// Usually registry tree paths are represented as a single vector of strings, here it's split into
121/// the first element (`base`) and the remainder (`path`) for convenience. `fs_base` is the file-
122/// system path of `base` (for example, if `base` is "dot", then `fs_base` is going to end in
123/// "/programs_py", since that's where all of the "dot" files are located within the Seahorse
124/// project.
125///
126/// The filesystem path of this path depends on whether the path will be used to retrieve a module
127/// or a package. Modules are located at `fs_base/base/(...path).py`, and packages are located at
128/// `fs_base/base/...path`.
129#[derive(Clone, Debug)]
130struct ComboPath {
131    base: String,
132    path: Vec<String>,
133    fs_base: PathBuf,
134}
135
136impl ComboPath {
137    fn new(mut path: Vec<String>, fs_base: PathBuf) -> Self {
138        let base = path.remove(0);
139
140        return Self {
141            base,
142            path,
143            fs_base,
144        };
145    }
146
147    /// Get the "full" path (base + path).
148    fn get_path(&self) -> Vec<String> {
149        let mut path = self.path.clone();
150        path.insert(0, self.base.clone());
151        return path;
152    }
153
154    /// Get the filesystem path for a module at this path
155    fn get_fs_module_path(&self) -> PathBuf {
156        let mut fs_path = self.fs_base.clone();
157        for part in self.path.iter().take(self.path.len() - 1) {
158            fs_path.push(part);
159        }
160        fs_path.push(format!("{}.py", self.path.last().unwrap()));
161
162        return fs_path;
163    }
164
165    /// Get the filesystem path for a package at this path
166    fn get_fs_package_path(&self) -> PathBuf {
167        let mut fs_path = self.fs_base.clone();
168        for part in self.path.iter() {
169            fs_path.push(part);
170        }
171
172        return fs_path;
173    }
174
175    fn push(&mut self, part: String) {
176        self.path.push(part);
177    }
178
179    fn pop(&mut self) {
180        self.path.pop();
181    }
182
183    fn is_module(&self) -> bool {
184        let fs_path = self.get_fs_module_path();
185        return fs_path.is_file();
186    }
187
188    fn is_package(&self) -> bool {
189        let fs_path = self.get_fs_package_path();
190        return fs_path.is_dir();
191    }
192}
193
194impl Display for ComboPath {
195    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
196        write!(
197            f,
198            "{} + {}",
199            self.fs_base.display(),
200            path_to_string(&self.path)
201        )
202    }
203}
204
205struct ModuleTreeBuilder {
206    tree: Tree<Module>,
207}
208
209impl ModuleTreeBuilder {
210    fn new() -> Self {
211        Self {
212            tree: Tree::Node(HashMap::new()),
213        }
214    }
215
216    /// Add a module.
217    fn add_module(&mut self, module: Module, path: ComboPath) -> CResult<()> {
218        match module {
219            Module::Python(module) => {
220                // Insert something as a placeholder, to detect overwriting during recursive calls
221                // (This also performs the check to see if the module is already added)
222                if !self
223                    .tree
224                    .insert(path.get_path(), Tree::Leaf(Module::SeahorsePrelude), false)
225                {
226                    return Ok(());
227                }
228
229                for Located(loc, obj) in module.statements.iter() {
230                    match obj {
231                        ca::TopLevelStatementObj::Import { symbols } => {
232                            for ca::ImportSymbol { symbol, .. } in symbols.iter() {
233                                // Try a Seahorse import
234                                {
235                                    let mut path =
236                                        ComboPath::new(vec!["sh".to_string()], PathBuf::new());
237                                    path.push(symbol.clone());
238
239                                    if self.tree.get(&path.get_path()).is_some() {
240                                        continue;
241                                    }
242                                }
243
244                                // Try a local import
245                                {
246                                    let mut path = path.clone();
247                                    path.pop();
248                                    path.push(symbol.clone());
249
250                                    if path.is_module() {
251                                        let module = load_module(path.get_fs_module_path())?;
252                                        self.add_module(module, path)?;
253                                    } else if path.is_package() {
254                                        self.add_package(path)?;
255                                    }
256                                    // TODO check for ext. modules
257                                    else {
258                                        return Err(Error::CouldNotFind(path)
259                                            .core()
260                                            .located(loc.clone()));
261                                    }
262                                }
263                            }
264                        }
265                        ca::TopLevelStatementObj::ImportFrom {
266                            level,
267                            path: symbol_path,
268                            ..
269                        } => {
270                            if *level > path.path.len() {
271                                return Err(Error::PathOutsideRoot(path, *level)
272                                    .core()
273                                    .located(loc.clone()));
274                            }
275
276                            if *level == path.path.len()
277                                && symbol_path.get(0).unwrap() == "seahorse"
278                            {
279                                return Err(Error::RelativeSeahorseImport
280                                    .core()
281                                    .located(loc.clone()));
282                            }
283
284                            // (Loop used so that nested scopes can immediately break here)
285                            'done: loop {
286                                // Try a Seahorse import
287                                if *level == 0 {
288                                    let mut path =
289                                        ComboPath::new(vec!["sh".to_string()], PathBuf::new());
290                                    for part in symbol_path.iter() {
291                                        path.push(part.clone());
292
293                                        if self.tree.get(&path.get_path()).is_some() {
294                                            break 'done;
295                                        }
296                                    }
297                                }
298
299                                // Try a local import
300                                {
301                                    let mut path = path.clone();
302                                    // Pop the module name
303                                    path.pop();
304                                    for _ in 0..*level {
305                                        path.pop();
306                                    }
307
308                                    for part in symbol_path.iter() {
309                                        path.push(part.clone());
310
311                                        if path.is_module() {
312                                            let module = load_module(path.get_fs_module_path())?;
313                                            self.add_module(module, path)?;
314                                            break 'done;
315                                        }
316                                    }
317
318                                    if path.is_package() {
319                                        self.add_package(path)?;
320                                    } else {
321                                        return Err(Error::CouldNotFind(path)
322                                            .core()
323                                            .located(loc.clone()));
324                                    }
325                                }
326                                break;
327                            }
328                        }
329                        _ => {}
330                    }
331                }
332
333                self.tree
334                    .insert(path.get_path(), Tree::Leaf(Module::Python(module)), true);
335            }
336            module => {
337                self.tree.insert(path.get_path(), Tree::Leaf(module), false);
338            }
339        }
340
341        return Ok(());
342    }
343
344    /// Add a package (by path).
345    ///
346    /// Assumes that `path` definitely points to a filesystem package path.
347    fn add_package(&mut self, path: ComboPath) -> CResult<()> {
348        let fs_path = path.get_fs_package_path();
349        for entry in read_dir(&fs_path).map_err(|_| Error::Os.core())? {
350            let entry = entry.map_err(|_| Error::Os.core())?;
351            let name = from_os_string(entry.file_name().as_os_str());
352
353            if entry.path().is_file() && name.ends_with(".py") {
354                let name = name.strip_suffix(".py").unwrap().to_string();
355                let mut path = path.clone();
356                path.push(name);
357
358                let module = load_module(path.get_fs_module_path())?;
359                self.add_module(module, path)?;
360            } else if entry.path().is_dir() {
361                let mut path = path.clone();
362                path.push(name);
363
364                if path.is_package() {
365                    self.add_package(path)?;
366                }
367            }
368        }
369
370        return Ok(());
371    }
372}
373
374/// Preprocess the source module by loading its dependencies and packaging them into a registry.
375pub fn preprocess(module: ca::Module, working_dir: PathBuf) -> Result<ModuleRegistry, CoreError> {
376    // The file system should have this structure:
377    // programs_py
378    // |\_ seahorse
379    // |   |\_ prelude.py
380    // |\_ program_name.py
381    //
382    // Which will be turned into this:
383    // (root)
384    // |\_ sh
385    // |   |\_ seahorse
386    // |       |\_ prelude
387    // |\_ dot
388    //     |\_ program
389    //
390    // sh and dot are the two registry sources. sh represents Seahorse builtins, and dot represents
391    // local files.
392    let mut builder = ModuleTreeBuilder::new();
393
394    builder.add_module(
395        Module::SeahorsePrelude,
396        ComboPath::new(
397            vec![
398                "sh".to_string(),
399                "seahorse".to_string(),
400                "prelude".to_string(),
401            ],
402            PathBuf::new(),
403        ),
404    )?;
405
406    builder.add_module(
407        Module::SeahorsePyth,
408        ComboPath::new(
409            vec!["sh".to_string(), "seahorse".to_string(), "pyth".to_string()],
410            PathBuf::new(),
411        ),
412    )?;
413
414    builder.add_module(
415        Module::Python(module),
416        ComboPath::new(vec!["dot".to_string(), "program".to_string()], working_dir),
417    )?;
418
419    let registry = ModuleRegistry {
420        tree: builder.tree,
421        origin: vec!["dot".to_string(), "program".to_string()],
422        order: vec!["sh".to_string(), "dot".to_string()],
423    };
424
425    return Ok(registry);
426}