koto_bytecode/
module_loader.rs

1use crate::{Chunk, Compiler, CompilerError, CompilerSettings};
2use dunce::canonicalize;
3use koto_memory::Ptr;
4use koto_parser::{KString, Span, format_source_excerpt};
5use rustc_hash::FxHasher;
6use std::{
7    collections::HashMap,
8    error, fmt,
9    hash::BuildHasherDefault,
10    io,
11    ops::Deref,
12    path::{Path, PathBuf},
13};
14use thiserror::Error;
15
16/// Errors that can be returned from [ModuleLoader] operations
17#[derive(Error, Debug)]
18#[allow(missing_docs)]
19pub enum ModuleLoaderErrorKind {
20    #[error("{0}")]
21    Compiler(#[from] CompilerError),
22    #[error("failed to canonicalize path '{path}' ({error})")]
23    FailedToCanonicalizePath { path: PathBuf, error: io::Error },
24    #[error("failed to read '{path}' ({error})")]
25    FailedToReadScript { path: PathBuf, error: io::Error },
26    #[error("failed to get current dir ({0}))")]
27    FailedToGetCurrentDir(io::Error),
28    #[error("failed to get parent of path ('{0}')")]
29    FailedToGetPathParent(PathBuf),
30    #[error("unable to find module '{0}'")]
31    UnableToFindModule(String),
32}
33
34/// The error type used by the [ModuleLoader]
35#[derive(Clone, Debug)]
36pub struct ModuleLoaderError {
37    /// The error
38    pub error: Ptr<ModuleLoaderErrorKind>,
39    /// The source of the error
40    pub source: Option<Ptr<LoaderErrorSource>>,
41}
42
43/// The source of a [ModuleLoaderError]
44#[derive(Debug)]
45pub struct LoaderErrorSource {
46    /// The script's contents
47    pub contents: String,
48    /// The span in the script where the error occurred
49    pub span: Span,
50    /// The script's path
51    pub path: Option<KString>,
52}
53
54impl ModuleLoaderError {
55    pub(crate) fn from_compiler_error(
56        error: CompilerError,
57        source: &str,
58        source_path: Option<KString>,
59    ) -> Self {
60        let source = LoaderErrorSource {
61            contents: source.into(),
62            span: error.span,
63            path: source_path,
64        };
65        Self {
66            error: ModuleLoaderErrorKind::from(error).into(),
67            source: Some(source.into()),
68        }
69    }
70
71    /// Returns true if the error was caused by the expectation of indentation during parsing
72    pub fn is_indentation_error(&self) -> bool {
73        match self.error.deref() {
74            ModuleLoaderErrorKind::Compiler(e) => e.is_indentation_error(),
75            _ => false,
76        }
77    }
78}
79
80impl fmt::Display for ModuleLoaderError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        writeln!(f, "{}.", self.error)?;
83        if let Some(source) = &self.source {
84            write!(
85                f,
86                "{}",
87                format_source_excerpt(&source.contents, &source.span, source.path.as_deref())
88            )?;
89        }
90        Ok(())
91    }
92}
93
94impl error::Error for ModuleLoaderError {}
95
96impl From<ModuleLoaderErrorKind> for ModuleLoaderError {
97    fn from(error: ModuleLoaderErrorKind) -> Self {
98        Self {
99            error: error.into(),
100            source: None,
101        }
102    }
103}
104
105/// Helper for loading, compiling, and caching Koto modules
106#[derive(Clone, Default)]
107pub struct ModuleLoader {
108    chunks: HashMap<PathBuf, Ptr<Chunk>, BuildHasherDefault<FxHasher>>,
109}
110
111impl ModuleLoader {
112    /// Compiles a script, deferring to [Compiler::compile]
113    pub fn compile_script(
114        &mut self,
115        script: &str,
116        script_path: Option<KString>,
117        settings: CompilerSettings,
118    ) -> Result<Ptr<Chunk>, ModuleLoaderError> {
119        Compiler::compile(script, script_path.clone(), settings)
120            .map(Ptr::from)
121            .map_err(|e| ModuleLoaderError::from_compiler_error(e, script, script_path))
122    }
123
124    /// Finds a module from its name, and then compiles it
125    pub fn compile_module(
126        &mut self,
127        module_name: &str,
128        current_script_path: Option<&Path>,
129    ) -> Result<CompileModuleResult, ModuleLoaderError> {
130        let module_path = find_module(module_name, current_script_path)?;
131
132        match self.chunks.get(&module_path) {
133            Some(chunk) => Ok(CompileModuleResult {
134                chunk: chunk.clone(),
135                path: module_path,
136                loaded_from_cache: true,
137            }),
138            None => {
139                let script = std::fs::read_to_string(&module_path).map_err(|error| {
140                    ModuleLoaderErrorKind::FailedToReadScript {
141                        path: module_path.clone(),
142                        error,
143                    }
144                })?;
145
146                let chunk = self.compile_script(
147                    &script,
148                    Some(module_path.clone().into()),
149                    CompilerSettings::default(),
150                )?;
151
152                self.chunks.insert(module_path.clone(), chunk.clone());
153
154                Ok(CompileModuleResult {
155                    chunk,
156                    path: module_path,
157                    loaded_from_cache: false,
158                })
159            }
160        }
161    }
162
163    /// Clears the compiled module cache
164    pub fn clear_cache(&mut self) {
165        self.chunks.clear();
166    }
167}
168
169/// Returned from [ModuleLoader::compile_module]
170pub struct CompileModuleResult {
171    /// The compiled module
172    pub chunk: Ptr<Chunk>,
173    // The path of the compiled module
174    pub path: PathBuf,
175    // True if the module was found in the [ModuleLoader] cache
176    pub loaded_from_cache: bool,
177}
178
179/// Finds a module that matches the given name
180///
181/// The `current_script_path` argument gives a location to start searching from,
182/// if `None` is provided then `std::env::current_dir` will be used instead.
183pub fn find_module(
184    module_name: &str,
185    current_script_path: Option<&Path>,
186) -> Result<PathBuf, ModuleLoaderError> {
187    // Get the directory of the provided script path, or the current working directory
188    let search_folder = match &current_script_path {
189        Some(path) => {
190            let canonicalized = canonicalize(path).map_err(|error| {
191                ModuleLoaderErrorKind::FailedToCanonicalizePath {
192                    path: path.to_path_buf(),
193                    error,
194                }
195            })?;
196            if canonicalized.is_file() {
197                match canonicalized.parent() {
198                    Some(parent_dir) => parent_dir.to_path_buf(),
199                    None => {
200                        let path = PathBuf::from(path);
201                        return Err(ModuleLoaderErrorKind::FailedToGetPathParent(path).into());
202                    }
203                }
204            } else {
205                canonicalized
206            }
207        }
208        None => std::env::current_dir().map_err(ModuleLoaderErrorKind::FailedToGetCurrentDir)?,
209    };
210
211    // First, check for a neighboring file with a matching name.
212    let extension = "koto";
213    let result = search_folder.join(module_name).with_extension(extension);
214    if result.exists() {
215        Ok(result)
216    } else {
217        // Alternatively, check for a neighboring directory with a matching name,
218        // that also contains a main file.
219        let result = search_folder
220            .join(module_name)
221            .join("main")
222            .with_extension(extension);
223        if result.exists() {
224            canonicalize(&result).map_err(|error| {
225                ModuleLoaderErrorKind::FailedToCanonicalizePath {
226                    path: result,
227                    error,
228                }
229                .into()
230            })
231        } else {
232            Err(ModuleLoaderErrorKind::UnableToFindModule(module_name.into()).into())
233        }
234    }
235}