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#[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#[derive(Clone, Debug)]
36pub struct ModuleLoaderError {
37 pub error: Ptr<ModuleLoaderErrorKind>,
39 pub source: Option<Ptr<LoaderErrorSource>>,
41}
42
43#[derive(Debug)]
45pub struct LoaderErrorSource {
46 pub contents: String,
48 pub span: Span,
50 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 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#[derive(Clone, Default)]
107pub struct ModuleLoader {
108 chunks: HashMap<PathBuf, Ptr<Chunk>, BuildHasherDefault<FxHasher>>,
109}
110
111impl ModuleLoader {
112 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 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 pub fn clear_cache(&mut self) {
165 self.chunks.clear();
166 }
167}
168
169pub struct CompileModuleResult {
171 pub chunk: Ptr<Chunk>,
173 pub path: PathBuf,
175 pub loaded_from_cache: bool,
177}
178
179pub fn find_module(
184 module_name: &str,
185 current_script_path: Option<&Path>,
186) -> Result<PathBuf, ModuleLoaderError> {
187 let search_folder = match ¤t_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 let extension = "koto";
213 let result = search_folder.join(module_name).with_extension(extension);
214 if result.exists() {
215 Ok(result)
216 } else {
217 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}