gruel_compiler/
prelude_source.rs1use include_dir::{Dir, include_dir};
23use std::path::{Path, PathBuf};
24
25static PRELUDE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../prelude");
27
28static STD_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../std");
30
31const PRELUDE_ROOT_REL: &str = "_prelude.gruel";
33
34const PRELUDE_SUBMODULE_ORDER: &[&str] = &[
42 "runtime.gruel",
45 "interfaces.gruel",
46 "target.gruel",
47 "type_info.gruel",
48 "cmp.gruel",
49 "option.gruel",
50 "result.gruel",
51 "char.gruel",
52 "vec.gruel",
53 "string.gruel",
54 "runtime_wrappers.gruel",
58];
59
60pub struct ResolvedPreludeFile {
62 pub path: String,
65 pub source: String,
67}
68
69pub struct ResolvedPrelude {
79 pub root: ResolvedPreludeFile,
81 pub prelude_dir: Vec<ResolvedPreludeFile>,
83 pub other_std_files: Vec<ResolvedPreludeFile>,
87}
88
89impl ResolvedPrelude {
90 pub fn aux_files(&self) -> impl Iterator<Item = &ResolvedPreludeFile> {
93 self.prelude_dir.iter().chain(self.other_std_files.iter())
94 }
95}
96
97pub fn resolved_prelude() -> ResolvedPrelude {
103 let disk_prelude = locate_dir("prelude", "GRUEL_PRELUDE_PATH", PRELUDE_ROOT_REL);
104 let disk_std = locate_dir("std", "GRUEL_STD_PATH", "_std.gruel");
105 if let (Some(prelude_dir), Some(std_dir)) = (disk_prelude.as_ref(), disk_std.as_ref())
106 && let Some(resolved) = read_disk(prelude_dir, std_dir)
107 {
108 return resolved;
109 }
110 embedded_prelude()
111}
112
113pub fn embedded_prelude() -> ResolvedPrelude {
115 let mut root: Option<ResolvedPreludeFile> = None;
116 let mut prelude_files: std::collections::HashMap<String, ResolvedPreludeFile> =
117 std::collections::HashMap::new();
118
119 for file in walk_dir(&PRELUDE_DIR) {
120 let rel = file.path().to_string_lossy().to_string();
121 let source = match file.contents_utf8() {
122 Some(s) => s.to_string(),
123 None => continue,
124 };
125 let path = format!("prelude/{}", rel);
126 let entry = ResolvedPreludeFile { path, source };
127 if rel == PRELUDE_ROOT_REL {
128 root = Some(entry);
129 } else if rel.ends_with(".gruel") {
130 prelude_files.insert(rel, entry);
131 }
132 }
133
134 let mut other_std_files = Vec::new();
135 for file in walk_dir(&STD_DIR) {
136 let rel = file.path().to_string_lossy().to_string();
137 if !rel.ends_with(".gruel") {
138 continue;
139 }
140 let source = match file.contents_utf8() {
141 Some(s) => s.to_string(),
142 None => continue,
143 };
144 let path = format!("std/{}", rel);
145 other_std_files.push(ResolvedPreludeFile { path, source });
146 }
147 other_std_files.sort_by(|a, b| a.path.cmp(&b.path));
148
149 arrange_prelude(
150 prelude_files,
151 root.expect("prelude/_prelude.gruel must exist"),
152 other_std_files,
153 )
154}
155
156fn arrange_prelude(
159 mut prelude_files: std::collections::HashMap<String, ResolvedPreludeFile>,
160 root: ResolvedPreludeFile,
161 other_std_files: Vec<ResolvedPreludeFile>,
162) -> ResolvedPrelude {
163 let mut prelude_dir = Vec::with_capacity(PRELUDE_SUBMODULE_ORDER.len());
164 for &rel in PRELUDE_SUBMODULE_ORDER {
165 if let Some(entry) = prelude_files.remove(rel) {
166 prelude_dir.push(entry);
167 }
168 }
169 let mut leftover: Vec<_> = prelude_files.keys().cloned().collect();
172 leftover.sort();
173 for rel in leftover {
174 if let Some(entry) = prelude_files.remove(&rel) {
175 prelude_dir.push(entry);
176 }
177 }
178 ResolvedPrelude {
179 root,
180 prelude_dir,
181 other_std_files,
182 }
183}
184
185fn walk_dir<'a>(dir: &'a Dir<'a>) -> Vec<&'a include_dir::File<'a>> {
188 let mut out = Vec::new();
189 walk_into(dir, &mut out);
190 out.sort_by_key(|f| f.path().to_path_buf());
191 out
192}
193
194fn walk_into<'a>(dir: &'a Dir<'a>, out: &mut Vec<&'a include_dir::File<'a>>) {
195 for entry in dir.entries() {
196 match entry {
197 include_dir::DirEntry::File(f) => out.push(f),
198 include_dir::DirEntry::Dir(d) => walk_into(d, out),
199 }
200 }
201}
202
203fn locate_dir(name: &str, env_var: &str, witness: &str) -> Option<PathBuf> {
206 if let Ok(env_path) = std::env::var(env_var) {
207 let candidate = PathBuf::from(&env_path);
208 if candidate.join(witness).exists() {
209 return Some(candidate);
210 }
211 }
212 let manifest = env!("CARGO_MANIFEST_DIR");
213 let mut current: PathBuf = PathBuf::from(manifest);
214 loop {
215 let candidate = current.join(name);
216 if candidate.join(witness).exists() {
217 return Some(candidate);
218 }
219 if !current.pop() {
220 break;
221 }
222 }
223 None
224}
225
226fn read_disk(prelude_dir: &Path, std_dir: &Path) -> Option<ResolvedPrelude> {
229 let root_path = prelude_dir.join(PRELUDE_ROOT_REL);
230 let root_source = std::fs::read_to_string(&root_path).ok()?;
231 let root = ResolvedPreludeFile {
232 path: root_path.to_string_lossy().into_owned(),
233 source: root_source,
234 };
235
236 let mut prelude_collected = Vec::new();
237 collect_gruel_files(prelude_dir, &mut prelude_collected);
238 prelude_collected.retain(|f| f.path != root.path);
239 let prelude_files: std::collections::HashMap<String, ResolvedPreludeFile> = prelude_collected
240 .into_iter()
241 .filter_map(|f| {
242 let rel = std::path::Path::new(&f.path)
243 .strip_prefix(prelude_dir)
244 .ok()?
245 .to_str()?
246 .to_string();
247 Some((rel, f))
248 })
249 .collect();
250
251 let mut other_std_files = Vec::new();
252 collect_gruel_files(std_dir, &mut other_std_files);
253 other_std_files.sort_by(|a, b| a.path.cmp(&b.path));
254
255 Some(arrange_prelude(prelude_files, root, other_std_files))
256}
257
258fn collect_gruel_files(dir: &Path, out: &mut Vec<ResolvedPreludeFile>) {
259 let entries = match std::fs::read_dir(dir) {
260 Ok(e) => e,
261 Err(_) => return,
262 };
263 for entry in entries.flatten() {
264 let path = entry.path();
265 if path.is_dir() {
266 collect_gruel_files(&path, out);
267 } else if path.extension().and_then(|s| s.to_str()) == Some("gruel")
268 && let Ok(source) = std::fs::read_to_string(&path)
269 {
270 out.push(ResolvedPreludeFile {
271 path: path.to_string_lossy().into_owned(),
272 source,
273 });
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn embedded_prelude_root_loadable() {
284 let p = embedded_prelude();
285 assert!(p.root.path.ends_with("_prelude.gruel"));
286 assert!(
288 p.root.path.contains("prelude/_prelude.gruel")
289 || p.root.path.contains("prelude\\_prelude.gruel")
290 );
291 }
292
293 #[test]
294 fn embedded_prelude_includes_submodules() {
295 let p = embedded_prelude();
296 assert!(
297 p.prelude_dir
298 .iter()
299 .any(|f| f.path.ends_with("/option.gruel") || f.path.ends_with("\\option.gruel"))
300 );
301 assert!(
302 p.prelude_dir
303 .iter()
304 .any(|f| f.path.ends_with("/cmp.gruel") || f.path.ends_with("\\cmp.gruel"))
305 );
306 }
307
308 #[test]
309 fn other_std_files_separate_from_prelude_dir() {
310 let p = embedded_prelude();
311 assert!(
312 p.other_std_files
313 .iter()
314 .any(|f| f.path.ends_with("_std.gruel"))
315 );
316 assert!(!p.prelude_dir.iter().any(|f| f.path.ends_with("_std.gruel")));
318 }
319}