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}