Skip to content

Commit 3355d31

Browse files
committed
feat: add data structure for dependency resolution
1 parent 1e16a74 commit 3355d31

3 files changed

Lines changed: 285 additions & 1 deletion

File tree

src/parse.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,6 +2385,19 @@ impl crate::ArbitraryRec for Match {
23852385
mod test {
23862386
use super::*;
23872387

2388+
impl UseDecl {
2389+
/// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution.
2390+
/// It automatically builds the internal Identifier path.
2391+
pub fn dummy_path(path: Vec<Identifier>) -> Self {
2392+
Self {
2393+
visibility: Visibility::default(),
2394+
path,
2395+
items: UseItems::List(Vec::new()),
2396+
span: Span::new(0, 0),
2397+
}
2398+
}
2399+
}
2400+
23882401
#[test]
23892402
fn test_reject_redefined_builtin_type() {
23902403
let ty = TypeAlias::parse_from_str("type Ctx8 = u32")

src/resolution.rs

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
use std::path::Path;
1+
use std::io;
2+
use std::path::{Path, PathBuf};
23
use std::sync::Arc;
34

5+
use crate::error::{Error, RichError, WithSpan as _};
6+
use crate::parse::UseDecl;
7+
48
/// Powers error reporting by mapping compiler diagnostics to the specific file.
59
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
610
pub struct SourceFile {
@@ -44,3 +48,259 @@ impl SourceFile {
4448
self.content.clone()
4549
}
4650
}
51+
52+
/// This defines how a specific dependency root path (e.g. "math")
53+
/// should be resolved to a physical path on the disk, restricted to
54+
/// files executing within the `context_prefix`.
55+
#[derive(Debug, Clone)]
56+
pub struct Remapping {
57+
/// The base directory that owns this dependency mapping.
58+
pub context_prefix: PathBuf,
59+
/// The name used in the `use` statement (e.g., "math").
60+
pub dependency_root_path: String,
61+
/// The physical path this dependency root path points to.
62+
pub target: PathBuf,
63+
}
64+
65+
/// A router for resolving dependencies across multi-file workspaces.
66+
///
67+
/// Mappings are strictly sorted by the longest `context_prefix` match.
68+
/// This mathematical guarantee ensures that if multiple nested directories
69+
/// define the same dependency root path, the most specific (deepest) context wins.
70+
#[derive(Debug, Default)]
71+
pub struct DependencyMap {
72+
inner: Vec<Remapping>,
73+
}
74+
75+
impl DependencyMap {
76+
pub fn new() -> Self {
77+
Self::default()
78+
}
79+
80+
pub fn is_empty(&self) -> bool {
81+
self.inner.is_empty()
82+
}
83+
84+
/// Re-sort the vector in descending order so the longest context paths are always at the front.
85+
/// This mathematically guarantees that the first match we find is the most specific.
86+
fn sort_mappings(&mut self) {
87+
self.inner.sort_by(|a, b| {
88+
let len_a = a.context_prefix.as_os_str().len();
89+
let len_b = b.context_prefix.as_os_str().len();
90+
len_b.cmp(&len_a)
91+
});
92+
}
93+
94+
/// Add a dependency mapped to a specific calling file's path prefix.
95+
/// Re-sorts the vector internally to guarantee the Longest Prefix Match.
96+
pub fn insert(
97+
&mut self,
98+
context: &Path,
99+
dependency_root_path: String,
100+
path: &Path,
101+
) -> io::Result<()> {
102+
let canon_context = std::fs::canonicalize(context).map_err(|err| {
103+
io::Error::new(
104+
err.kind(),
105+
format!(
106+
"Failed to find context directory '{}': {}",
107+
context.display(),
108+
err
109+
),
110+
)
111+
})?;
112+
113+
let canon_path = std::fs::canonicalize(path).map_err(|err| {
114+
io::Error::new(
115+
err.kind(),
116+
format!(
117+
"Failed to find library target path '{}': {}",
118+
path.display(),
119+
err
120+
),
121+
)
122+
})?;
123+
124+
self.inner.push(Remapping {
125+
context_prefix: canon_context,
126+
dependency_root_path,
127+
target: canon_path,
128+
});
129+
130+
self.sort_mappings();
131+
132+
Ok(())
133+
}
134+
135+
/// Resolve `use dependency_root_path::...` into a physical file path by finding the
136+
/// most specific library context that owns the current file.
137+
pub fn resolve_path(
138+
&self,
139+
current_file: &Path,
140+
use_decl: &UseDecl,
141+
) -> Result<PathBuf, RichError> {
142+
// Safely extract the first segment (the dependency root path)
143+
let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_inner()).collect();
144+
let first_segment = parts.first().copied().ok_or_else(|| {
145+
Error::CannotParse("Empty use path".to_string()).with_span(*use_decl.span())
146+
})?;
147+
148+
// Because the vector is sorted by longest prefix,
149+
// the VERY FIRST match we find is guaranteed to be the correct one.
150+
for remapping in &self.inner {
151+
// Check if the current file is executing inside the context's directory tree.
152+
// This prevents a file in `/project_a/` from using a dependency meant for `/project_b/`
153+
if !current_file.starts_with(&remapping.context_prefix) {
154+
continue;
155+
}
156+
157+
// Check if the alias matches what the user typed
158+
if remapping.dependency_root_path == first_segment {
159+
let mut resolved_path = remapping.target.clone();
160+
resolved_path.extend(&parts[1..]);
161+
return Ok(resolved_path);
162+
}
163+
}
164+
165+
// No matches found
166+
Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span())
167+
}
168+
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use crate::str::Identifier;
173+
174+
use super::*;
175+
176+
// --- TEST HELPERS ---
177+
178+
impl DependencyMap {
179+
/// Inserts a dependency remapping without interacting with the physical file system.
180+
pub fn test_insert_without_canonicalize(
181+
&mut self,
182+
context: &Path,
183+
dependency_root_path: String,
184+
path: &Path,
185+
) {
186+
self.inner.push(Remapping {
187+
context_prefix: context.to_path_buf(),
188+
dependency_root_path,
189+
target: path.to_path_buf(),
190+
});
191+
self.sort_mappings();
192+
}
193+
}
194+
195+
/// Helper to easily construct a `UseDecl` for path resolution tests.
196+
fn create_dummy_use_decl(path_segments: &[&str]) -> UseDecl {
197+
// Convert string slices into `Identifier` struct.
198+
// If your macro generates a different constructor, just swap `From::from`
199+
// with `Identifier::new(s)` or `Identifier(Arc::from(s))`!
200+
let path: Vec<Identifier> = path_segments
201+
.iter()
202+
.map(|&s| Identifier::dummy(s))
203+
.collect();
204+
205+
UseDecl::dummy_path(path)
206+
}
207+
208+
// --- TESTS ---
209+
210+
#[test]
211+
fn test_sorting_longest_prefix() {
212+
let mut map = DependencyMap::new();
213+
214+
map.test_insert_without_canonicalize(
215+
Path::new("/workspace"),
216+
"math".to_string(),
217+
Path::new("/lib/math_v1"),
218+
);
219+
map.test_insert_without_canonicalize(
220+
Path::new("/workspace/project_a/nested"),
221+
"math".to_string(),
222+
Path::new("/lib/math_v3"),
223+
);
224+
map.test_insert_without_canonicalize(
225+
Path::new("/workspace/project_a"),
226+
"math".to_string(),
227+
Path::new("/lib/math_v2"),
228+
);
229+
230+
assert_eq!(
231+
map.inner[0].context_prefix,
232+
Path::new("/workspace/project_a/nested")
233+
);
234+
assert_eq!(
235+
map.inner[1].context_prefix,
236+
Path::new("/workspace/project_a")
237+
);
238+
assert_eq!(map.inner[2].context_prefix, Path::new("/workspace"));
239+
}
240+
241+
#[test]
242+
fn test_context_isolation() {
243+
let mut map = DependencyMap::new();
244+
map.test_insert_without_canonicalize(
245+
Path::new("/project_a"),
246+
"utils".to_string(),
247+
Path::new("/libs/utils_a"),
248+
);
249+
250+
let current_file = Path::new("/project_b/main.simf");
251+
let use_decl = create_dummy_use_decl(&["utils"]);
252+
253+
let result = map.resolve_path(current_file, &use_decl);
254+
255+
assert!(result.is_err());
256+
assert!(matches!(
257+
result.unwrap_err().error(),
258+
Error::UnknownLibrary(..)
259+
));
260+
}
261+
262+
#[test]
263+
fn test_resolve_longest_prefix_match() {
264+
let mut map = DependencyMap::new();
265+
266+
map.test_insert_without_canonicalize(
267+
Path::new("/workspace"),
268+
"math".to_string(),
269+
Path::new("/libs/global_math"),
270+
);
271+
map.test_insert_without_canonicalize(
272+
Path::new("/workspace/frontend"),
273+
"math".to_string(),
274+
Path::new("/libs/frontend_math"),
275+
);
276+
277+
// 1. Frontend file -> should resolve to frontend_math
278+
let frontend_file = Path::new("/workspace/frontend/src/main.simf");
279+
let use_decl = create_dummy_use_decl(&["math", "vector"]);
280+
281+
let resolved_frontend = map.resolve_path(frontend_file, &use_decl).unwrap();
282+
assert_eq!(resolved_frontend, Path::new("/libs/frontend_math/vector"));
283+
284+
// 2. Backend file -> should bypass frontend and hit global_math
285+
let backend_file = Path::new("/workspace/backend/src/main.simf");
286+
287+
let resolved_backend = map.resolve_path(backend_file, &use_decl).unwrap();
288+
assert_eq!(resolved_backend, Path::new("/libs/global_math/vector"));
289+
}
290+
291+
#[test]
292+
fn test_resolve_subpath_extension() {
293+
let mut map = DependencyMap::new();
294+
map.test_insert_without_canonicalize(
295+
Path::new("/app"),
296+
"core".to_string(),
297+
Path::new("/vendor/core_lib"),
298+
);
299+
300+
let current_file = Path::new("/app/main.simf");
301+
let use_decl = create_dummy_use_decl(&["core", "network", "http"]);
302+
303+
let resolved = map.resolve_path(current_file, &use_decl).unwrap();
304+
assert_eq!(resolved, Path::new("/vendor/core_lib/network/http"));
305+
}
306+
}

src/str.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,14 @@ impl ModuleName {
293293
}
294294

295295
wrapped_string!(ModuleName, "module name");
296+
297+
#[cfg(test)]
298+
mod tests {
299+
use super::*;
300+
301+
impl Identifier {
302+
pub fn dummy(name: &str) -> Self {
303+
Self(std::sync::Arc::from(name))
304+
}
305+
}
306+
}

0 commit comments

Comments
 (0)