Skip to main content

gruel_air/sema/
visibility.rs

1//! Visibility checking for module system.
2//!
3//! This module implements the visibility rules defined in ADR-0026:
4//! - `pub` items are always accessible
5//! - Private items are accessible if the files are in the same directory module
6
7use std::path::{Path, PathBuf};
8
9use gruel_util::FileId;
10use gruel_util::{CompileError, CompileResult, ErrorKind};
11
12use crate::types::EnumId;
13
14use super::Sema;
15
16impl Sema<'_> {
17    /// Check if the accessing file can see a private item from the target file.
18    ///
19    /// Visibility rules (per ADR-0026):
20    /// - `pub` items are always accessible
21    /// - Private items are accessible if the files are in the same directory module
22    ///
23    /// Directory module membership includes:
24    /// - Files directly in the directory (e.g., `utils/strings.gruel` is in `utils`)
25    /// - Facade files for the directory (e.g., `_utils.gruel` is in `utils` module)
26    ///
27    /// Returns true if the item is accessible.
28    /// ADR-0079: is this file part of the privileged prelude?
29    /// Mirrors the predicate in `lang_items::is_directive_in_prelude`
30    /// — the reserved-id band catches submodules loaded by
31    /// `prepend_prelude` before file_paths registration, plus any
32    /// path that satisfies `is_prelude_path`.
33    pub(crate) fn is_prelude_file(&self, file_id: FileId) -> bool {
34        if file_id.index() >= 0xFFFF_F000 {
35            return true;
36        }
37        match self.get_file_path(file_id) {
38            Some(path) => super::file_paths::is_prelude_path(path),
39            None => false,
40        }
41    }
42
43    pub(crate) fn is_accessible(
44        &self,
45        accessing_file_id: FileId,
46        target_file_id: FileId,
47        is_pub: bool,
48    ) -> bool {
49        // Public items are always accessible
50        if is_pub {
51            return true;
52        }
53
54        // ADR-0073: a non-pub item homed in the synthetic `<builtin>`
55        // module is unreachable from any user file. The builtin sentinel
56        // FileId is never registered in `file_paths`, so the
57        // unknown-paths fallback below would otherwise be permissive —
58        // short-circuit explicitly.
59        if target_file_id == FileId::BUILTIN && accessing_file_id != FileId::BUILTIN {
60            return false;
61        }
62
63        // ADR-0079: prelude code is privileged — it can read non-pub
64        // fields and call non-pub methods on user types. The
65        // prelude-implemented `derive Clone` (and future similar
66        // derives) need this so their spliced bodies can construct
67        // `Self { … }` against the host type even when its fields
68        // aren't `pub`.
69        if self.is_prelude_file(accessing_file_id) {
70            return true;
71        }
72
73        // Get paths for both files
74        let accessing_path = self.get_file_path(accessing_file_id);
75        let target_path = self.get_file_path(target_file_id);
76
77        // If we can't determine the paths, be permissive (for single-file mode or tests)
78        match (accessing_path, target_path) {
79            (Some(acc), Some(tgt)) => {
80                // Get the "module identity" for each file.
81                // For a regular file like `utils/strings.gruel`, the module is `utils/`
82                // For a facade file like `_utils.gruel`, the module is `utils/` (the directory it represents)
83                let acc_module = get_module_identity(Path::new(acc));
84                let tgt_module = get_module_identity(Path::new(tgt));
85
86                acc_module == tgt_module
87            }
88            // If either path is unknown, allow access (e.g., synthetic types, single-file mode)
89            _ => true,
90        }
91    }
92
93    /// Resolve an enum type through a module reference.
94    ///
95    /// Used for qualified enum paths like `module.EnumName::Variant` in match patterns.
96    /// Checks visibility: private enums are only accessible from the same directory.
97    pub fn resolve_enum_through_module(
98        &self,
99        _module_ref: gruel_rir::InstRef,
100        type_name: lasso::Spur,
101        span: gruel_util::Span,
102    ) -> CompileResult<EnumId> {
103        let type_name_str = self.interner.resolve(&type_name);
104
105        // Try to find the enum globally
106        let enum_id = self.enums.get(&type_name).copied().ok_or_else(|| {
107            CompileError::new(ErrorKind::UnknownEnumType(type_name_str.to_string()), span)
108        })?;
109
110        // Check visibility
111        let enum_def = self.type_pool.enum_def(enum_id);
112        let accessing_file_id = span.file_id;
113        let target_file_id = enum_def.file_id;
114
115        if !self.is_accessible(accessing_file_id, target_file_id, enum_def.is_pub) {
116            return Err(CompileError::new(
117                ErrorKind::PrivateMemberAccess {
118                    item_kind: "enum".to_string(),
119                    name: type_name_str.to_string(),
120                },
121                span,
122            ));
123        }
124
125        Ok(enum_id)
126    }
127}
128
129/// Get the module identity for a file path.
130///
131/// - For regular files: returns the parent directory
132/// - For facade files (`_foo.gruel`): returns the corresponding directory (`foo/`)
133///
134/// This allows facade files to be treated as part of their corresponding directory module.
135pub(crate) fn get_module_identity(path: &Path) -> Option<PathBuf> {
136    let parent = path.parent()?;
137    let file_stem = path.file_stem()?.to_str()?;
138
139    // Check if this is a facade file (starts with underscore)
140    if let Some(module_name) = file_stem.strip_prefix('_') {
141        // Facade file: _utils.gruel -> parent/utils
142        // Strip the leading underscore
143        Some(parent.join(module_name))
144    } else {
145        // Regular file: the module is just the parent directory
146        Some(parent.to_path_buf())
147    }
148}