Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,15 +389,16 @@ pub(crate) mod tests {
let lib_dir = canon(&ws.create_dir("workspace/libs/lib"));

// Set up the dependency map for imports (e.g. `use lib::...`)
let mut map = DependencyMap::new();
map.insert(workspace_dir.clone(), "lib".to_string(), lib_dir.clone())
.expect("Failed to insert dependency map");
let remappings = vec![crate::resolution::Remapping {
context_prefix: workspace_dir.clone(),
drp_name: "lib".to_string(),
target: lib_dir.clone(),
}];
let mut map = DependencyMap::try_from(remappings).expect("Failed to create dependency map");

// Register the strict crate boundaries so local files are forced to use `crate::`
map.insert(workspace_dir.clone(), CRATE_STR.to_string(), workspace_dir)
.expect("Failed to insert workspace crate boundary");
map.insert(lib_dir.clone(), CRATE_STR.to_string(), lib_dir)
.expect("Failed to insert library crate boundary");
map.add_crate_root(workspace_dir);
map.add_crate_root(lib_dir);
let map = Arc::new(map);

let mut root_file_path = None;
Expand Down
36 changes: 15 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,31 +494,31 @@ pub(crate) mod tests {
I: IntoIterator<Item = (P, K, P)>,
K: Into<String>,
{
let mut dependency_map = DependencyMap::new();
let mut remappings = Vec::new();
let mut crate_roots = Vec::new();

if let Some(parent) = prog_path.as_ref().parent() {
let canon_root = crate::resolution::tests::canon(parent);
let _ = dependency_map.insert(
canon_root.clone(),
crate::driver::CRATE_STR.to_string(),
canon_root,
);
crate_roots.push(canon_root);
}

for (context, alias, target) in dependencies {
let context = crate::resolution::tests::canon(context.as_ref());
let target = crate::resolution::tests::canon(target.as_ref());

dependency_map
.insert(context.clone(), alias.into(), target.clone())
.unwrap();
remappings.push(crate::resolution::Remapping {
context_prefix: context,
drp_name: alias.into(),
target: target.clone(),
});

// Treat each mapped dependency as an isolated external package to satisfy strict local-file checks
let _ = dependency_map.insert(
target.clone(),
crate::driver::CRATE_STR.to_string(),
target,
);
crate_roots.push(target);
}

let mut dependency_map = DependencyMap::try_from(remappings).unwrap();
for root in crate_roots {
dependency_map.add_crate_root(root);
}

TestCase::<TemplateProgram>::template_deps(prog_path.as_ref(), &dependency_map)
Expand Down Expand Up @@ -742,13 +742,7 @@ pub(crate) mod tests {
let main_path = root.join("main.simf");
let mut dependency_map = DependencyMap::new();
let canon_root = CanonPath::canonicalize(&root).unwrap();
dependency_map
.insert(
canon_root.clone(),
crate::driver::CRATE_STR.to_string(),
canon_root,
)
.unwrap();
dependency_map.add_crate_root(canon_root);

TestCase::<TemplateProgram>::template_deps(&main_path, &dependency_map)
.with_arguments(Arguments::default())
Expand Down
36 changes: 23 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use base64::engine::general_purpose::STANDARD;
use clap::{Arg, ArgAction, Command};

use simplicityhl::{
driver::CRATE_STR,
resolution::{CanonPath, DependencyMap, SourceFile},
AbiMeta, CompiledProgram,
};
Expand Down Expand Up @@ -129,16 +128,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.get_many::<String>("dependencies")
.unwrap_or_default();

let mut dependencies = DependencyMap::new();

// Automatically assign the `crate` root to the project directory
let canon_root = main_path
.as_path()
.parent()
.and_then(|p| CanonPath::canonicalize(p).ok());
if let Some(ref canon) = canon_root {
let _ = dependencies.insert(canon.clone(), CRATE_STR.to_string(), canon.clone());
}

let mut remappings = Vec::new();
let mut target_paths = Vec::new();

for arg in dep_args {
let (left_side, path_str) = arg.split_once('=').unwrap_or_else(|| {
Expand All @@ -164,17 +160,31 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

let target_path = CanonPath::canonicalize(Path::new(path_str))?;

if let Err(e) = dependencies.insert(context_path, alias.to_string(), target_path.clone()) {
eprintln!("Error: {e}");
std::process::exit(1);
}
remappings.push(simplicityhl::resolution::Remapping {
context_prefix: context_path,
drp_name: alias.to_string(),
target: target_path.clone(),
});

// Treat the external package as an isolated boundary, allowing it to use `crate::` internally
if let Err(e) = dependencies.insert(target_path.clone(), CRATE_STR.to_string(), target_path)
{
target_paths.push(target_path);
}

let mut dependencies = match DependencyMap::try_from(remappings) {
Ok(map) => map,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};

// Automatically assign the `crate` root to the project directory
if let Some(ref canon) = canon_root {
dependencies.add_crate_root(canon.clone());
}

for target_path in target_paths {
dependencies.add_crate_root(target_path);
}

let source = SourceFile::new(main_path.as_path(), std::sync::Arc::from(main_text));
Expand Down
141 changes: 102 additions & 39 deletions src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ pub struct DependencyMap {
inner: Vec<Remapping>,
}

impl TryFrom<Vec<Remapping>> for DependencyMap {
type Error = io::Error;

fn try_from(mappings: Vec<Remapping>) -> Result<Self, Self::Error> {
let mut map = DependencyMap::new();
for m in mappings {
map.insert(m.context_prefix, m.drp_name, m.target)?;
}
Ok(map)
}
}

impl DependencyMap {
pub fn new() -> Self {
Self::default()
Expand All @@ -135,6 +147,19 @@ impl DependencyMap {
self.inner.is_empty()
}

/// Safely sets the `crate` root for a given path.
///
/// This configures a directory to act as an isolated package boundary,
/// enabling internal absolute imports via `crate::`.
pub fn add_crate_root(&mut self, root: CanonPath) {
self.inner.push(Remapping {
context_prefix: root.clone(),
drp_name: CRATE_STR.to_string(),
target: root,
});
self.sort_mappings();
}

/// Re-sort the vector in descending order so the longest context paths are always at the front.
/// This mathematically guarantees that the first match we find is the most specific.
fn sort_mappings(&mut self) {
Expand All @@ -156,12 +181,19 @@ impl DependencyMap {
/// programmer types in their source code (e.g., the `"math"` in `use math::vector;`).
/// * `target` - The physical directory where the compiler should actually
/// look for the code (e.g., `/libs/frontend_math`).
pub fn insert(
pub(crate) fn insert(
&mut self,
context: CanonPath,
drp_name: String,
target: CanonPath,
) -> io::Result<()> {
if drp_name == CRATE_STR {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("The '{}' keyword is reserved and cannot be manually mapped using `insert`. Use `add_crate_root` instead.", CRATE_STR),
));
}

self.inner.push(Remapping {
context_prefix: context,
drp_name,
Expand Down Expand Up @@ -304,6 +336,24 @@ pub(crate) mod tests {
UseDecl::dummy_path(path)
}

/// Attempting to manually map the `crate` keyword using `insert()` must result in an error.
#[test]
fn test_insert_crate_fails() {
let ws = TempWorkspace::new("insert_crate_fail");
let project_dir = canon(&ws.create_dir("workspace"));

let mappings = vec![Remapping {
context_prefix: project_dir.clone(),
drp_name: CRATE_STR.to_string(),
target: project_dir.clone(),
}];

let result = DependencyMap::try_from(mappings);

assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("reserved"));
}

/// When a user registers the same library dependency root path multiple times
/// for different folders, the compiler must always check the longest folder path first.
#[test]
Expand All @@ -318,13 +368,24 @@ pub(crate) mod tests {
let target_v3 = canon(&ws.create_dir("lib/math_v3"));
let target_v2 = canon(&ws.create_dir("lib/math_v2"));

let mut map = DependencyMap::new();
map.insert(workspace_dir.clone(), "math".to_string(), target_v1)
.unwrap();
map.insert(nested_dir.clone(), "math".to_string(), target_v3)
.unwrap();
map.insert(project_a_dir.clone(), "math".to_string(), target_v2)
.unwrap();
let mappings = vec![
Remapping {
context_prefix: workspace_dir.clone(),
drp_name: "math".to_string(),
target: target_v1,
},
Remapping {
context_prefix: nested_dir.clone(),
drp_name: "math".to_string(),
target: target_v3,
},
Remapping {
context_prefix: project_a_dir.clone(),
drp_name: "math".to_string(),
target: target_v2,
},
];
let map = DependencyMap::try_from(mappings).unwrap();

// The longest prefixes should bubble to the top
assert_eq!(map.inner[0].context_prefix, nested_dir);
Expand All @@ -342,9 +403,12 @@ pub(crate) mod tests {
let target_utils = canon(&ws.create_dir("libs/utils_a"));
let current_file = canon(&ws.create_file("project_b/main.simf", ""));

let mut map = DependencyMap::new();
map.insert(project_a, "utils".to_string(), target_utils)
.unwrap();
let mappings = vec![Remapping {
context_prefix: project_a,
drp_name: "utils".to_string(),
target: target_utils,
}];
let map = DependencyMap::try_from(mappings).unwrap();

let use_decl = create_dummy_use_decl(&["utils"]);
let result = map.resolve_path(&current_file, &use_decl);
Expand Down Expand Up @@ -372,11 +436,19 @@ pub(crate) mod tests {
let frontend_target = canon(&ws.create_dir("libs/frontend_math"));
let frontend_expected = canon(&ws.create_file("libs/frontend_math/vector.simf", ""));

let mut map = DependencyMap::new();
map.insert(global_context, "math".to_string(), global_target)
.unwrap();
map.insert(frontend_context, "math".to_string(), frontend_target)
.unwrap();
let mappings = vec![
Remapping {
context_prefix: global_context,
drp_name: "math".to_string(),
target: global_target,
},
Remapping {
context_prefix: frontend_context,
drp_name: "math".to_string(),
target: frontend_target,
},
];
let map = DependencyMap::try_from(mappings).unwrap();

let use_decl = create_dummy_use_decl(&["math", "vector"]);

Expand All @@ -400,21 +472,13 @@ pub(crate) mod tests {
ws.create_file("workspace/utils.simf", "");
let current_file = canon(&ws.create_file("workspace/main.simf", ""));

let mut map = DependencyMap::new();
// The driver sets up the crate root
map.insert(
project_dir.clone(),
CRATE_STR.to_string(),
project_dir.clone(),
)
.unwrap();
// The user tries to alias a folder inside their own project as an external dependency
map.insert(
project_dir.clone(),
"utils_lib".to_string(),
project_dir.clone(),
)
.unwrap();
let mappings = vec![Remapping {
context_prefix: project_dir.clone(),
drp_name: "utils_lib".to_string(),
target: project_dir.clone(),
}];
let mut map = DependencyMap::try_from(mappings).unwrap();
map.add_crate_root(project_dir.clone());

let use_decl = create_dummy_use_decl(&["utils_lib", "utils"]);
let result = map.resolve_path(&current_file, &use_decl);
Expand All @@ -436,12 +500,7 @@ pub(crate) mod tests {
let current_file = canon(&ws.create_file("workspace/main.simf", ""));

let mut map = DependencyMap::new();
map.insert(
project_dir.clone(),
CRATE_STR.to_string(),
project_dir.clone(),
)
.unwrap();
map.add_crate_root(project_dir.clone());

let use_decl = create_dummy_use_decl(&[CRATE_STR, "utils"]);
let result = map.resolve_path(&current_file, &use_decl).unwrap();
Expand Down Expand Up @@ -480,8 +539,12 @@ pub(crate) mod tests {

let current_file = canon(&ws.create_file("workspace/frontend/src/main.simf", ""));

let mut map = DependencyMap::new();
map.insert(context, "math".to_string(), target).unwrap();
let mappings = vec![Remapping {
context_prefix: context,
drp_name: "math".to_string(),
target,
}];
let map = DependencyMap::try_from(mappings).unwrap();

let use_decl = create_dummy_use_decl(&["math", "vector"]);
let result = map.resolve_path(&current_file, &use_decl).unwrap();
Expand Down
29 changes: 29 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,32 @@ fn cli_dependency_can_use_crate_root() {
String::from_utf8_lossy(&output.stderr),
);
}

#[test]
fn cli_reserved_crate_mapping_fails() {
let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate");
let main = root.join("main.simf");
let ext_lib = root.join("ext_lib");

// Attempt to maliciously override the `crate` keyword
let dep_arg = format!("crate={}", ext_lib.display());

let output = Command::new(env!("CARGO_BIN_EXE_simc"))
.arg(main)
.arg("--dep")
.arg(dep_arg)
.output()
.expect("failed to run simc");

assert!(
!output.status.success(),
"simc unexpectedly succeeded when overriding the 'crate' dependency"
);

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("keyword is reserved"),
"Expected 'keyword is reserved' error, got:\n{}",
stderr
);
}
Loading