koto_bytecode/
module_loader.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
use crate::{Chunk, Compiler, CompilerError, CompilerSettings};
use dunce::canonicalize;
use koto_memory::Ptr;
use koto_parser::{format_source_excerpt, KString, Span};
use rustc_hash::FxHasher;
use std::{
    collections::HashMap,
    error, fmt,
    hash::BuildHasherDefault,
    io,
    ops::Deref,
    path::{Path, PathBuf},
};
use thiserror::Error;

/// Errors that can be returned from [ModuleLoader] operations
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum ModuleLoaderErrorKind {
    #[error("{0}")]
    Compiler(#[from] CompilerError),
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error("Failed to get parent of path ('{0}')")]
    FailedToGetPathParent(PathBuf),
    #[error("Unable to find module '{0}'")]
    UnableToFindModule(String),
}

/// The error type used by the [ModuleLoader]
#[derive(Clone, Debug)]
pub struct ModuleLoaderError {
    /// The error
    pub error: Ptr<ModuleLoaderErrorKind>,
    /// The source of the error
    pub source: Option<Ptr<LoaderErrorSource>>,
}

/// The source of a [ModuleLoaderError]
#[derive(Debug)]
pub struct LoaderErrorSource {
    /// The script's contents
    pub contents: String,
    /// The span in the script where the error occurred
    pub span: Span,
    /// The script's path
    pub path: Option<KString>,
}

impl ModuleLoaderError {
    pub(crate) fn from_compiler_error(
        error: CompilerError,
        source: &str,
        source_path: Option<KString>,
    ) -> Self {
        let source = LoaderErrorSource {
            contents: source.into(),
            span: error.span,
            path: source_path,
        };
        Self {
            error: ModuleLoaderErrorKind::from(error).into(),
            source: Some(source.into()),
        }
    }

    /// Returns true if the error was caused by the expectation of indentation during parsing
    pub fn is_indentation_error(&self) -> bool {
        match self.error.deref() {
            ModuleLoaderErrorKind::Compiler(e) => e.is_indentation_error(),
            _ => false,
        }
    }
}

impl fmt::Display for ModuleLoaderError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}.", self.error)?;
        if let Some(source) = &self.source {
            write!(
                f,
                "{}",
                format_source_excerpt(&source.contents, &source.span, source.path.as_deref())
            )?;
        }
        Ok(())
    }
}

impl error::Error for ModuleLoaderError {}

impl From<io::Error> for ModuleLoaderError {
    fn from(error: io::Error) -> Self {
        Self::from(ModuleLoaderErrorKind::Io(error))
    }
}

impl From<ModuleLoaderErrorKind> for ModuleLoaderError {
    fn from(error: ModuleLoaderErrorKind) -> Self {
        Self {
            error: error.into(),
            source: None,
        }
    }
}

/// Helper for loading, compiling, and caching Koto modules
#[derive(Clone, Default)]
pub struct ModuleLoader {
    chunks: HashMap<PathBuf, Ptr<Chunk>, BuildHasherDefault<FxHasher>>,
}

impl ModuleLoader {
    /// Compiles a script, deferring to [Compiler::compile]
    pub fn compile_script(
        &mut self,
        script: &str,
        script_path: Option<KString>,
        settings: CompilerSettings,
    ) -> Result<Ptr<Chunk>, ModuleLoaderError> {
        Compiler::compile(script, script_path.clone(), settings)
            .map_err(|e| ModuleLoaderError::from_compiler_error(e, script, script_path))
    }

    /// Finds a module from its name, and then compiles it
    pub fn compile_module(
        &mut self,
        module_name: &str,
        current_script_path: Option<&Path>,
    ) -> Result<CompileModuleResult, ModuleLoaderError> {
        let module_path = find_module(module_name, current_script_path)?;

        match self.chunks.get(&module_path) {
            Some(chunk) => Ok(CompileModuleResult {
                chunk: chunk.clone(),
                path: module_path,
                loaded_from_cache: true,
            }),
            None => {
                let script = std::fs::read_to_string(&module_path)?;

                let chunk = self.compile_script(
                    &script,
                    Some(module_path.clone().into()),
                    CompilerSettings::default(),
                )?;

                self.chunks.insert(module_path.clone(), chunk.clone());

                Ok(CompileModuleResult {
                    chunk,
                    path: module_path,
                    loaded_from_cache: false,
                })
            }
        }
    }

    /// Clears the compiled module cache
    pub fn clear_cache(&mut self) {
        self.chunks.clear();
    }
}

/// Returned from [ModuleLoader::compile_module]
pub struct CompileModuleResult {
    /// The compiled module
    pub chunk: Ptr<Chunk>,
    // The path of the compiled module
    pub path: PathBuf,
    // True if the module was found in the [ModuleLoader] cache
    pub loaded_from_cache: bool,
}

/// Finds a module that matches the given name
///
/// The `current_script_path` argument gives a location to start searching from,
/// if `None` is provided then `std::env::current_dir` will be used instead.
pub fn find_module(
    module_name: &str,
    current_script_path: Option<&Path>,
) -> Result<PathBuf, ModuleLoaderError> {
    // Get the directory of the provided script path, or the current working directory
    let search_folder = match &current_script_path {
        Some(path) => {
            let canonicalized = canonicalize(path)?;
            if canonicalized.is_file() {
                match canonicalized.parent() {
                    Some(parent_dir) => parent_dir.to_path_buf(),
                    None => {
                        let path = PathBuf::from(path);
                        return Err(ModuleLoaderErrorKind::FailedToGetPathParent(path).into());
                    }
                }
            } else {
                canonicalized
            }
        }
        None => std::env::current_dir()?,
    };

    // First, check for a neighboring file with a matching name.
    let extension = "koto";
    let result = search_folder.join(module_name).with_extension(extension);
    if result.exists() {
        Ok(result)
    } else {
        // Alternatively, check for a neighboring directory with a matching name,
        // that also contains a main file.
        let result = search_folder
            .join(module_name)
            .join("main")
            .with_extension(extension);
        if result.exists() {
            let result = canonicalize(result)?;
            Ok(result)
        } else {
            Err(ModuleLoaderErrorKind::UnableToFindModule(module_name.into()).into())
        }
    }
}