Skip to main content

gruel_compiler/
prelude_source.rs

1//! Prelude and stdlib source resolution (ADR-0079).
2//!
3//! The Gruel prelude is a top-level module rooted at `prelude/_prelude.gruel`.
4//! That single file is implicitly `@import`-ed by every compilation: its
5//! `pub` items become available in user files without an explicit import.
6//! Internally, `_prelude.gruel` uses `@import` + `pub const` re-exports to
7//! organize itself across sibling `prelude/*.gruel` submodules.
8//!
9//! Stdlib lives at `std/` and is a regular library — reachable via
10//! `@import("std")`, with no auto-load semantics. Splitting prelude and
11//! stdlib at the file layer (ADR-0079) makes the privilege boundary
12//! explicit: only files under `prelude/` are allowed to claim
13//! `@lang(...)` bindings.
14//!
15//! Both trees are embedded into the binary via `include_dir!` so the
16//! compiler ships self-contained. When `GRUEL_STD_PATH` /
17//! `GRUEL_PRELUDE_PATH` is set or the binary runs from inside a
18//! checked-out repo, on-disk files win over the embedded copy so
19//! contributors editing prelude or stdlib code get their changes
20//! without rebuilding the compiler.
21
22use include_dir::{Dir, include_dir};
23use std::path::{Path, PathBuf};
24
25/// Embedded copy of the top-level `prelude/` directory tree.
26static PRELUDE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../prelude");
27
28/// Embedded copy of the top-level `std/` directory tree.
29static STD_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../std");
30
31/// Path within `prelude/` to the prelude root.
32const PRELUDE_ROOT_REL: &str = "_prelude.gruel";
33
34/// Order in which prelude submodules must be loaded.
35///
36/// Today's resolver collects function signatures in source order, so a
37/// function whose return type references `Result(...)` has to follow
38/// `result.gruel` in the merged AST. This list encodes the dependency
39/// order; any unlisted `.gruel` files under `prelude/` are loaded
40/// alphabetically after the listed ones.
41const PRELUDE_SUBMODULE_ORDER: &[&str] = &[
42    // ADR-0087: extern bindings to libc and the Gruel runtime archive.
43    // Loaded first so other prelude modules can reference the bindings.
44    "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    // ADR-0087: thin Gruel wrappers over the surviving Rust-runtime
55    // helpers. Loaded after `string.gruel` because the wrapper
56    // signatures (e.g. `dbg_str(s: Ref(String))`) reference `String`.
57    "runtime_wrappers.gruel",
58];
59
60/// One source file with its path and content.
61pub struct ResolvedPreludeFile {
62    /// Path used by the module resolver — disk-absolute when on-disk,
63    /// virtual `prelude/<rel>` or `std/<rel>` otherwise.
64    pub path: String,
65    /// Source content.
66    pub source: String,
67}
68
69/// Result of locating the prelude.
70///
71/// The compiler stages every entry in `aux_files` into the compilation
72/// unit's `file_paths` so `@import` resolution finds them. `prelude_dir`
73/// lists the files under `prelude/` (excluding the root) — the ones
74/// referenced by `_prelude.gruel`'s re-exports — so test fixtures that
75/// bypass `CompilationUnit::parse` can inline their items without
76/// dragging in unrelated stdlib modules (which would have unresolved
77/// `@import` references in a test environment).
78pub struct ResolvedPrelude {
79    /// `prelude/_prelude.gruel` itself — the file the compiler implicitly imports.
80    pub root: ResolvedPreludeFile,
81    /// Files under `prelude/`, in dependency-aware order.
82    pub prelude_dir: Vec<ResolvedPreludeFile>,
83    /// Stdlib files under `std/` (e.g. `_std.gruel`, `math.gruel`).
84    /// Pre-staged for `@import("std")` and friends. Empty in test
85    /// fixtures.
86    pub other_std_files: Vec<ResolvedPreludeFile>,
87}
88
89impl ResolvedPrelude {
90    /// Every file other than the root, in load order. Used by the
91    /// compilation driver to register all stdlib paths.
92    pub fn aux_files(&self) -> impl Iterator<Item = &ResolvedPreludeFile> {
93        self.prelude_dir.iter().chain(self.other_std_files.iter())
94    }
95}
96
97/// Resolve the prelude.
98///
99/// Tries on-disk `prelude/` and `std/` first (via `GRUEL_PRELUDE_PATH` /
100/// `GRUEL_STD_PATH` or an upward search from the binary's manifest dir);
101/// falls back to the embedded trees otherwise.
102pub 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
113/// Embedded prelude (the `include_dir!` fallback).
114pub 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
156/// Arrange prelude submodules into dependency-aware order; pass through
157/// the `other_std_files` list as the caller computed it.
158fn 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    // Any leftover prelude files (added later, not yet in
170    // PRELUDE_SUBMODULE_ORDER) — append alphabetically.
171    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
185/// Iterate every file (recursive) inside an `include_dir` tree, sorted by
186/// path for deterministic ordering.
187fn 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
203/// Try to locate a top-level workspace directory (e.g. `prelude/` or
204/// `std/`) on disk.
205fn 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
226/// Read every `.gruel` file under `prelude_dir` and `std_dir`. Returns
227/// `None` if the prelude root is missing — caller falls back to embedded.
228fn 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        // The root path lives under prelude/, not std/.
287        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        // Prelude_dir should not contain stdlib files.
317        assert!(!p.prelude_dir.iter().any(|f| f.path.ends_with("_std.gruel")));
318    }
319}