diff --git a/src/ast.rs b/src/ast.rs index fca0947f..573edcb9 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1754,7 +1754,8 @@ mod alias_scope_regression_tests { use crate::error::ErrorCollector; fn analyze_multifile(files: Vec<(&str, &str)>) -> Result<(), String> { - let (graph, _ids, _dir) = setup_graph(files); + let (graph, _ids, _dir) = + setup_graph(files, [crate::unstable::UnstableFeature::UseKeyword]); let mut error_handler = ErrorCollector::new(); let driver_program = graph diff --git a/src/driver/linearization.rs b/src/driver/linearization.rs index 7e6dc413..e78d7a1e 100644 --- a/src/driver/linearization.rs +++ b/src/driver/linearization.rs @@ -88,15 +88,23 @@ impl fmt::Display for LinearizationError { #[cfg(test)] mod tests { use crate::driver::tests::setup_graph; + use crate::unstable::UnstableFeature; use super::*; #[test] fn test_linearize_simple_import() { - let (graph, ids, _dir) = setup_graph(vec![ - ("main.simf", "use lib::math::some_func;"), - ("libs/lib/math.simf", ""), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let order = graph.linearize().unwrap(); @@ -114,12 +122,19 @@ mod tests { // B -> imports Common // Expected: Common loaded ONLY ONCE. - let (graph, ids, _dir) = setup_graph(vec![ - ("main.simf", "use lib::A::foo; use lib::B::bar;"), - ("libs/lib/A.simf", "use crate::Common::dummy1;"), - ("libs/lib/B.simf", "use crate::Common::dummy2;"), - ("libs/lib/Common.simf", ""), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use crate::Common::dummy1;"), + ("libs/lib/B.simf", "use crate::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let order = graph.linearize().unwrap(); @@ -137,11 +152,18 @@ mod tests { #[test] fn test_linearize_detects_cycle() { - let (graph, _, _dir) = setup_graph(vec![ - ("main.simf", "use lib::A::entry;"), - ("libs/lib/A.simf", "use crate::B::func;"), - ("libs/lib/B.simf", "use crate::A::func;"), - ]); + let (graph, _, _dir) = setup_graph( + vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use crate::B::func;"), + ("libs/lib/B.simf", "use crate::A::func;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let order = graph.linearize(); assert!(matches!( @@ -154,13 +176,20 @@ mod tests { fn test_linearize_allows_conflicting_nested_import_order() { // A imports X then Y, while B imports Y then X. // This DAG is still valid because neither X nor Y depends on the other. - let (graph, ids, _dir) = setup_graph(vec![ - ("main.simf", "use lib::A::foo; use lib::B::bar;"), - ("libs/lib/A.simf", "use crate::X::foo; use crate::Y::bar;"), - ("libs/lib/B.simf", "use crate::Y::baz; use crate::X::qux;"), - ("libs/lib/X.simf", ""), - ("libs/lib/Y.simf", ""), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use crate::X::foo; use crate::Y::bar;"), + ("libs/lib/B.simf", "use crate::Y::baz; use crate::X::qux;"), + ("libs/lib/X.simf", ""), + ("libs/lib/Y.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let order = graph .linearize() diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 0f026f84..5411a5ec 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -40,6 +40,7 @@ use crate::error::{Error, ErrorCollector, RichError, Span}; use crate::parse::{self, ParseFromStrWithErrors}; use crate::resolution::DependencyMap; use crate::source::{CanonPath, CanonSourceFile}; +use crate::UnstableFeatureManager; pub use crate::driver::resolve_order::{FileScoped, Program, SymbolTable}; @@ -139,6 +140,7 @@ impl DependencyGraph { dependency_map: Arc, root_program: &parse::Program, handler: &mut ErrorCollector, + unstable_manager: &UnstableFeatureManager, ) -> Result, String> { let mut graph = Self { modules: vec![Module { @@ -187,6 +189,7 @@ impl DependencyGraph { &importer_source, handler, &mut queue, + unstable_manager, ); } @@ -204,6 +207,7 @@ impl DependencyGraph { importer_source: &CanonSourceFile, span: Span, handler: &mut ErrorCollector, + unstable_manager: &UnstableFeatureManager, ) -> Option { let Ok(content) = std::fs::read_to_string(path.as_path()) else { let err = RichError::new( @@ -223,6 +227,10 @@ impl DependencyGraph { let ast = parse::Program::parse_from_str_with_errors(source.clone(), &mut error_handler); + if let Some(ref p) = ast { + unstable_manager.check_program(p, &mut error_handler); + } + if error_handler.has_errors() { handler.extend_with_handler(source, &error_handler); None @@ -260,6 +268,7 @@ impl DependencyGraph { /// PHASE 2 OF GRAPH CONSTRUCTION: Loads, parses, and registers new dependencies. /// Note: This is a specialized helper function designed exclusively for the `DependencyGraph::new()` constructor. + #[allow(clippy::too_many_arguments)] fn load_and_parse_dependencies( &mut self, curr_id: usize, @@ -268,6 +277,7 @@ impl DependencyGraph { importer_source: &CanonSourceFile, handler: &mut ErrorCollector, queue: &mut VecDeque, + unstable_manager: &UnstableFeatureManager, ) { for (path, import_span) in valid_imports { if inalid_imports.contains(&path) { @@ -282,9 +292,13 @@ impl DependencyGraph { continue; } - let Some(module) = - Self::parse_and_get_program(&path, importer_source, import_span, handler) - else { + let Some(module) = Self::parse_and_get_program( + &path, + importer_source, + import_span, + handler, + unstable_manager, + ) else { inalid_imports.push(path); continue; }; @@ -307,6 +321,7 @@ pub(crate) mod tests { use crate::resolution::tests::canon; use crate::resolution::DependencyMapBuilder; use crate::test_utils::TempWorkspace; + use crate::unstable::UnstableFeature; /// Initializes a raw graph environment for testing, explicitly allowing for and capturing failure states. /// @@ -330,6 +345,7 @@ pub(crate) mod tests { /// 4. `ErrorCollector`: The handler containing any logged errors, useful fo pub(crate) fn setup_graph_raw( files: Vec<(&str, &str)>, + features: impl IntoIterator, ) -> ( Option, HashMap, @@ -366,16 +382,31 @@ pub(crate) mod tests { let root_p = root_file_path.expect("main.simf must be defined in file list"); let main_canon_source = CanonSourceFile::new(root_p, Arc::from(root_content)); + let main_source = main_canon_source.clone(); + + let unstable_manager = UnstableFeatureManager::new(features); let main_program_option = - parse::Program::parse_from_str_with_errors(main_canon_source.clone(), &mut handler); + parse::Program::parse_from_str_with_errors(main_source.clone(), &mut handler); + + if let Some(ref p) = main_program_option { + let mut unstable_errors = ErrorCollector::new(); + unstable_manager.check_program(p, &mut unstable_errors); + handler.extend_with_handler(main_source.clone(), &unstable_errors); + } let Some(main_program) = main_program_option else { return (None, HashMap::new(), ws, handler); }; - let graph_option = - DependencyGraph::new(main_canon_source, map, &main_program, &mut handler).unwrap(); + let graph_option = DependencyGraph::new( + main_canon_source, + map, + &main_program, + &mut handler, + &unstable_manager, + ) + .unwrap(); let mut file_ids = HashMap::new(); @@ -418,8 +449,9 @@ pub(crate) mod tests { /// to standard error if the parser or graph builder encounters any issues. pub(crate) fn setup_graph( files: Vec<(&str, &str)>, + features: impl IntoIterator, ) -> (DependencyGraph, HashMap, TempWorkspace) { - let (graph_option, file_ids, ws, handler) = setup_graph_raw(files); + let (graph_option, file_ids, ws, handler) = setup_graph_raw(files, features); let Some(graph) = graph_option else { panic!( @@ -437,10 +469,17 @@ pub(crate) mod tests { // root.simf -> "use lib::math::some_func;" // libs/lib/math.simf -> "" - let (graph, ids, _ws) = setup_graph(vec![ - ("main.simf", "use lib::math::some_func;"), - ("libs/lib/math.simf", ""), - ]); + let (graph, ids, _ws) = setup_graph( + vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); @@ -464,12 +503,19 @@ pub(crate) mod tests { // B -> imports Common // Expected: Common loaded ONLY ONCE. - let (graph, ids, _ws) = setup_graph(vec![ - ("main.simf", "use lib::A::foo; use lib::B::bar;"), - ("libs/lib/A.simf", "use crate::Common::dummy1;"), - ("libs/lib/B.simf", "use crate::Common::dummy2;"), - ("libs/lib/Common.simf", ""), - ]); + let (graph, ids, _ws) = setup_graph( + vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use crate::Common::dummy1;"), + ("libs/lib/B.simf", "use crate::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); // Check strict deduplication (Unique modules count) assert_eq!( @@ -509,11 +555,18 @@ pub(crate) mod tests { // A -> imports B // B -> imports A - let (graph, ids, _ws) = setup_graph(vec![ - ("main.simf", "use lib::A::entry;"), - ("libs/lib/A.simf", "use crate::B::func;"), - ("libs/lib/B.simf", "use crate::A::func;"), - ]); + let (graph, ids, _ws) = setup_graph( + vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use crate::B::func;"), + ("libs/lib/B.simf", "use crate::A::func;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let a_id = ids["A"]; let b_id = ids["B"]; @@ -540,8 +593,14 @@ pub(crate) mod tests { // Setup: root imports from "unknown", which is not in our dependency map. // We use `setup_graph_raw` because we expect graph generation to fail and // emit an error, rather than panicking the standard test helper. - let (graph_option, _ids, _ws, handler) = - setup_graph_raw(vec![("main.simf", "use unknown::library::item;")]); + let (graph_option, _ids, _ws, handler) = setup_graph_raw( + vec![("main.simf", "use unknown::library::item;")], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); assert!( graph_option.is_none(), @@ -559,11 +618,18 @@ pub(crate) mod tests { // Goal: Verify that a simple chain (main -> a -> b) correctly pushes items // into the vectors and builds the adjacency list in BFS order. - let (graph, ids, _ws) = setup_graph(vec![ - ("main.simf", "use lib::A::mock_item;"), - ("libs/lib/A.simf", "use crate::B::mock_item;"), - ("libs/lib/B.simf", ""), - ]); + let (graph, ids, _ws) = setup_graph( + vec![ + ("main.simf", "use lib::A::mock_item;"), + ("libs/lib/A.simf", "use crate::B::mock_item;"), + ("libs/lib/B.simf", ""), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); // Assert: Size checks assert_eq!(graph.modules.len(), 3); @@ -597,4 +663,39 @@ pub(crate) mod tests { .map_or(true, |deps| deps.is_empty()); assert!(b_has_no_deps, "B depends on nothing"); } + + #[test] + fn test_unstable_feature_use_keyword_disabled_in_dependencies() { + // We use setup_graph_raw because it intentionally bypasses the early-exit in + // TemplateProgram::new_with_dep, allowing us to test how the graph builder + // deeply checks for unstable feature errors in loaded dependency files. + let (graph_option, _ids, _ws, handler) = setup_graph_raw( + vec![ + ("main.simf", "use lib::A::foo;\nfn main() {}"), + ("libs/lib/A.simf", "use crate::B::bar;\npub fn foo() {}"), + ("libs/lib/B.simf", "pub fn bar() {}"), + ], + [], // No unstable features enabled! + ); + + assert!( + graph_option.is_none(), + "Graph generation should fail due to unstable feature errors" + ); + + let errors = handler.to_string(); + + // Assert that both main.simf and A.simf reported unstable feature errors + assert!( + errors.contains("main.simf"), + "Should report error in main.simf" + ); + assert!( + errors.contains("A.simf"), + "Should report error in dependency A.simf" + ); + + // Three occurrences of the error message total (1 for main.simf `use`, 2 for A.simf `use` and `crate`) + assert_eq!(errors.matches("feature is not enabled").count(), 3); + } } diff --git a/src/driver/resolve_order.rs b/src/driver/resolve_order.rs index ad5463d5..a24753c7 100644 --- a/src/driver/resolve_order.rs +++ b/src/driver/resolve_order.rs @@ -520,6 +520,7 @@ impl AsRef for Program { #[cfg(test)] mod resolve_order_tests { use crate::driver::tests::setup_graph; + use crate::unstable::UnstableFeature; use super::*; @@ -528,10 +529,14 @@ mod resolve_order_tests { // main.simf defines a private function and a public function. // Expected: Both should appear in the scope with correct visibility. - let (graph, ids, _dir) = setup_graph(vec![( - "main.simf", - "fn private_fn() {} pub fn public_fn() {}", - )]); + let (graph, ids, _dir) = setup_graph( + vec![("main.simf", "fn private_fn() {} pub fn public_fn() {}")], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -560,11 +565,18 @@ mod resolve_order_tests { // 3. main.simf imports it from B. // Expected: B's scope must contain `foo` marked as Public. - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub use crate::A::foo;"), - ("main.simf", "use lib::B::foo;"), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub use crate::A::foo;"), + ("main.simf", "use lib::B::foo;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -595,11 +607,18 @@ mod resolve_order_tests { // 3. main.simf tries to import `foo` from B. // Expected: Error, because B did not re-export foo. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "use crate::A::foo;"), // <--- Private binding! - ("main.simf", "use lib::B::foo;"), // <--- Should fail - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "use crate::A::foo;"), // <--- Private binding! + ("main.simf", "use lib::B::foo;"), // <--- Should fail + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -616,10 +635,17 @@ mod resolve_order_tests { #[test] fn test_separated_type_aliases_and_functions() { - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32; pub fn bar() {}"), - ("main.simf", "use lib::A::bar;"), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub type bar = u32; pub fn bar() {}"), + ("main.simf", "use lib::A::bar;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -647,11 +673,18 @@ mod resolve_order_tests { // main.simf: load function `foo` from A.simf. // Then try to load both `fn foo` and `type foo`. // However, we have already loade `fn foo` and `type foo` is private, so an error occurs. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub fn foo() {} type foo = u32;"), - ("main.simf", "use lib::A::foo; use lib::B::foo;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub fn foo() {} type foo = u32;"), + ("main.simf", "use lib::A::foo; use lib::B::foo;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -668,7 +701,14 @@ mod resolve_order_tests { // Scenario: A user tries to declare the entry point as `pub fn main`. // Expected: The compiler must reject this because `main` must be private. - let (graph, _ids, _dir) = setup_graph(vec![("main.simf", "pub fn main() {}")]); + let (graph, _ids, _dir) = setup_graph( + vec![("main.simf", "pub fn main() {}")], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -691,10 +731,17 @@ mod resolve_order_tests { // Scenario: A user tries to bypass entry point rules by renaming an import to `main`. // Expected: The compiler must reject this because `main` is a reserved identifier. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32;"), - ("main.simf", "use lib::A::bar as main;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub type bar = u32;"), + ("main.simf", "use lib::A::bar as main;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -717,6 +764,7 @@ mod resolve_order_tests { mod alias_tests { use super::*; use crate::driver::tests::setup_graph; + use crate::unstable::UnstableFeature; #[test] fn test_renaming_with_use() { @@ -724,10 +772,17 @@ mod alias_tests { // main.simf: use lib::A::foo as bar; // Expected: Scope should contain "bar", but not "foo". - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("main.simf", "use lib::A::foo as bar;"), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("main.simf", "use lib::A::foo as bar;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -756,10 +811,17 @@ mod alias_tests { #[test] fn test_multiple_aliases_in_list() { // Scenario: Renaming multiple imports inside brackets. - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"), - ("main.simf", "use lib::A::{foo as bar, baz as qux};"), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"), + ("main.simf", "use lib::A::{foo as bar, baz as qux};"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -791,10 +853,17 @@ mod alias_tests { #[test] fn test_alias_private_item_fails() { // Scenario: Attempting to alias a private item should fail. - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "fn secret() {}"), // Note: Missing `pub` - ("main.simf", "use lib::A::secret as my_secret;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "fn secret() {}"), // Note: Missing `pub` + ("main.simf", "use lib::A::secret as my_secret;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -815,11 +884,18 @@ mod alias_tests { #[test] fn test_deep_reexport_with_aliases() { // Scenario: Chaining aliases across multiple files. - let (graph, ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn original() {}"), - ("libs/lib/B.simf", "pub use crate::A::original as middle;"), - ("main.simf", "use lib::B::middle as final_name;"), - ]); + let (graph, ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn original() {}"), + ("libs/lib/B.simf", "pub use crate::A::original as middle;"), + ("main.simf", "use lib::B::middle as final_name;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -859,12 +935,19 @@ mod alias_tests { #[test] fn test_deep_reexport_private_link_fails() { // Scenario: Main tries to import an alias from B, but B's alias is private! - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn target() {}"), - // Note: Missing `pub` keyword here! This makes `hidden_alias` private to B. - ("libs/lib/B.simf", "use crate::A::target as hidden_alias;"), - ("main.simf", "use lib::B::hidden_alias;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn target() {}"), + // Note: Missing `pub` keyword here! This makes `hidden_alias` private to B. + ("libs/lib/B.simf", "use crate::A::target as hidden_alias;"), + ("main.simf", "use lib::B::hidden_alias;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -885,12 +968,19 @@ mod alias_tests { #[test] fn test_alias_cycle_detection() { // Scenario: A malicious or confused user creates an infinite alias/import loop. - let (graph, _ids, _dir) = setup_graph(vec![ - // A imports from B, B imports from A. This creates a file-level cycle! - ("libs/lib/A.simf", "pub use crate::B::pong as ping;"), - ("libs/lib/B.simf", "pub use crate::A::ping as pong;"), - ("main.simf", "use lib::A::ping;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + // A imports from B, B imports from A. This creates a file-level cycle! + ("libs/lib/A.simf", "pub use crate::B::pong as ping;"), + ("libs/lib/B.simf", "pub use crate::A::ping as pong;"), + ("main.simf", "use lib::A::ping;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); @@ -920,11 +1010,18 @@ mod alias_tests { #[test] fn test_plain_import_and_alias_to_same_name_is_rejected() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn foo() {}"), - ("libs/lib/B.simf", "pub fn foo() {}"), - ("main.simf", "use lib::A::foo; use lib::B::foo as foo;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub fn foo() {}"), + ("main.simf", "use lib::A::foo; use lib::B::foo as foo;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -943,14 +1040,21 @@ mod alias_tests { #[test] fn test_failed_alias_import_does_not_poison_following_imports() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn nope() {}"), - ("libs/lib/B.simf", "pub fn bar() {}"), - ( - "main.simf", - "use lib::A::missing as foo; use lib::B::bar as foo;", - ), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn nope() {}"), + ("libs/lib/B.simf", "pub fn bar() {}"), + ( + "main.simf", + "use lib::A::missing as foo; use lib::B::bar as foo;", + ), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -969,10 +1073,17 @@ mod alias_tests { #[test] fn test_alias_cannot_reuse_local_definition_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn bar() {}"), - ("main.simf", "pub fn foo() {} use lib::A::bar as foo;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn bar() {}"), + ("main.simf", "pub fn foo() {} use lib::A::bar as foo;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -993,10 +1104,17 @@ mod alias_tests { #[test] fn test_local_function_cannot_reuse_alias_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub fn bar() {}"), - ("main.simf", "use lib::A::bar as foo; pub fn foo() {}"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub fn bar() {}"), + ("main.simf", "use lib::A::bar as foo; pub fn foo() {}"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); @@ -1016,10 +1134,17 @@ mod alias_tests { #[test] fn test_local_type_alias_cannot_reuse_alias_name() { - let (graph, _ids, _dir) = setup_graph(vec![ - ("libs/lib/A.simf", "pub type bar = u32;"), - ("main.simf", "use lib::A::bar as foo; type foo = u64;"), - ]); + let (graph, _ids, _dir) = setup_graph( + vec![ + ("libs/lib/A.simf", "pub type bar = u32;"), + ("main.simf", "use lib::A::bar as foo; type foo = u64;"), + ], + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); let mut error_handler = ErrorCollector::new(); let program_option = graph.linearize_and_build(&mut error_handler).unwrap(); diff --git a/src/error.rs b/src/error.rs index 83420973..d8ac084a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -475,6 +475,9 @@ impl fmt::Display for ErrorCollector { /// Records _what_ happened but not where. #[derive(Debug, Clone)] pub enum Error { + UnstableFeature { + feature_name: String, + }, DependencyPathNotFound { path: PathBuf, }, @@ -640,6 +643,10 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::UnstableFeature { feature_name } => write!( + f, + "The '{feature_name}' feature is not enabled.\nEnable it with: -Z {feature_name}" + ), Error::DependencyPathNotFound { path } => write!( f, "Path not found: {}", path.display() diff --git a/src/lib.rs b/src/lib.rs index 707ce622..4cf1654f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod parse; pub mod pattern; pub mod resolution; pub mod source; +pub mod unstable; #[cfg(feature = "serde")] mod serde; @@ -45,6 +46,7 @@ use crate::resolution::DependencyMap; use crate::source::CanonSourceFile; use crate::source::SourceFile; pub use crate::types::ResolvedType; +pub use crate::unstable::{UnstableFeature, UnstableFeatureManager}; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; @@ -68,6 +70,25 @@ impl TemplateProgram { source: CanonSourceFile, dependency_map: &DependencyMap, jet_hinter: Box, + ) -> Result { + Self::with_unstable_and_dep( + source, + dependency_map, + &UnstableFeatureManager::default(), + jet_hinter, + ) + } + + /// Parse the template of a SimplicityHL program with explicit unstable features enabled. + /// + /// ## Errors + /// + /// The string is not a valid SimplicityHL program. + pub fn with_unstable_and_dep( + source: CanonSourceFile, + dependency_map: &DependencyMap, + unstable_manager: &UnstableFeatureManager, + jet_hinter: Box, ) -> Result { let mut error_handler = ErrorCollector::new(); @@ -76,12 +97,18 @@ impl TemplateProgram { parse::Program::parse_from_str_with_errors(source.clone(), &mut error_handler) .ok_or_else(|| error_handler.to_string())?; - // 2. Create the driver program + // 2. Check unstable features and record errors with precise spans + let mut unstable_errors = ErrorCollector::new(); + unstable_manager.check_program(&parsed_program, &mut unstable_errors); + error_handler.extend_with_handler(source.clone(), &unstable_errors); + + // 3. Create the driver program let graph = DependencyGraph::new( source.clone(), Arc::from(dependency_map.clone()), &parsed_program, &mut error_handler, + unstable_manager, )? .ok_or_else(|| error_handler.to_string())?; @@ -89,7 +116,7 @@ impl TemplateProgram { .linearize_and_build(&mut error_handler)? .ok_or_else(|| error_handler.to_string())?; - // 3. AST Analysis + // 4. AST Analysis let ast_program = ast::Program::analyze(&driver_program, jet_hinter.clone_box()) .with_source(source.clone())?; Ok(Self { @@ -107,29 +134,43 @@ impl TemplateProgram { pub fn new>>( s: Str, jet_hinter: Box, + ) -> Result { + Self::with_unstable(s, &UnstableFeatureManager::default(), jet_hinter) + } + + /// Parse the template of a SimplicityHL program with explicit unstable features enabled. + /// + /// ## Errors + /// + /// The string is not a valid SimplicityHL program. + pub fn with_unstable>>( + s: Str, + unstable_manager: &UnstableFeatureManager, + jet_hinter: Box, ) -> Result { let file = s.into(); let source = SourceFile::anonymous(file.clone()); let mut error_handler = ErrorCollector::new(); - let parse_program = parse::Program::parse_from_str_with_errors(source, &mut error_handler); - - let driver_program = if let Some(parse_program) = parse_program { - driver::Program::from_parse(&parse_program, file.clone(), &mut error_handler) - } else { - None - }; - - if let Some(program) = driver_program { - let ast_program = ast::Program::analyze(&program, jet_hinter.clone_box()) - .with_content(Arc::clone(&file))?; - Ok(Self { - simfony: ast_program, - file, - jet_hinter, - }) - } else { - Err(ErrorCollector::to_string(&error_handler))? - } + + let parsed_program = + parse::Program::parse_from_str_with_errors(source.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())?; + + let mut unstable_errors = ErrorCollector::new(); + unstable_manager.check_program(&parsed_program, &mut unstable_errors); + error_handler.extend_with_handler(source.clone(), &unstable_errors); + + let driver_program = + driver::Program::from_parse(&parsed_program, file.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())?; + + let ast_program = ast::Program::analyze(&driver_program, jet_hinter.clone_box()) + .with_content(Arc::clone(&file))?; + Ok(Self { + simfony: ast_program, + file, + jet_hinter, + }) } /// Access the parameters of the program. @@ -205,8 +246,32 @@ impl CompiledProgram { include_debug_symbols: bool, jet_hinter: Box, ) -> Result { - TemplateProgram::new_with_dep(source, dependency_map, jet_hinter.clone_box()) - .and_then(|template| template.instantiate(arguments, include_debug_symbols)) + Self::with_unstable_and_dep( + source, + dependency_map, + &UnstableFeatureManager::default(), + arguments, + include_debug_symbols, + jet_hinter, + ) + } + + /// Parse and compile a SimplicityHL program with explicit unstable features enabled. + pub fn with_unstable_and_dep( + source: CanonSourceFile, + dependency_map: &DependencyMap, + unstable_manager: &UnstableFeatureManager, + arguments: Arguments, + include_debug_symbols: bool, + jet_hinter: Box, + ) -> Result { + TemplateProgram::with_unstable_and_dep( + source, + dependency_map, + unstable_manager, + jet_hinter.clone_box(), + ) + .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } /// Parse and compile a SimplicityHL program from the given string. @@ -221,7 +286,24 @@ impl CompiledProgram { include_debug_symbols: bool, jet_hinter: Box, ) -> Result { - TemplateProgram::new(s, jet_hinter.clone_box()) + Self::with_unstable( + s, + &UnstableFeatureManager::default(), + arguments, + include_debug_symbols, + jet_hinter, + ) + } + + /// Parse and compile a SimplicityHL program with explicit unstable features enabled. + pub fn with_unstable>>( + s: Str, + unstable_manager: &UnstableFeatureManager, + arguments: Arguments, + include_debug_symbols: bool, + jet_hinter: Box, + ) -> Result { + TemplateProgram::with_unstable(s, unstable_manager, jet_hinter.clone_box()) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -307,7 +389,32 @@ impl SatisfiedProgram { include_debug_symbols: bool, jet_hinter: Box, ) -> Result { - let compiled = CompiledProgram::new(s, arguments, include_debug_symbols, jet_hinter)?; + Self::with_unstable( + s, + &UnstableFeatureManager::default(), + arguments, + witness_values, + include_debug_symbols, + jet_hinter, + ) + } + + /// Parse, compile and satisfy a SimplicityHL program with explicit unstable features enabled. + pub fn with_unstable>>( + s: Str, + unstable_manager: &UnstableFeatureManager, + arguments: Arguments, + witness_values: WitnessValues, + include_debug_symbols: bool, + jet_hinter: Box, + ) -> Result { + let compiled = CompiledProgram::with_unstable( + s, + unstable_manager, + arguments, + include_debug_symbols, + jet_hinter, + )?; compiled.satisfy(witness_values) } @@ -435,20 +542,52 @@ pub(crate) mod tests { impl TestCase { pub fn template_file>(program_file_path: P) -> Self { + Self::template_file_with_unstable( + program_file_path, + std::iter::empty::(), + ) + } + + pub fn template_file_with_unstable, F>( + program_file_path: P, + features: F, + ) -> Self + where + F: IntoIterator + Clone, + { let program_text = std::fs::read_to_string(program_file_path).unwrap(); - Self::template_text(Cow::Owned(program_text)) + Self::template_text_with_unstable(Cow::Owned(program_text), features) } + // Temporary allow while imports are unstable + #[allow(dead_code)] pub fn template_deps(prog_path: &Path, dependency_map: &DependencyMap) -> Self { + Self::template_deps_with_unstable( + prog_path, + dependency_map, + std::iter::empty::(), + ) + } + + pub fn template_deps_with_unstable( + prog_path: &Path, + dependency_map: &DependencyMap, + features: F, + ) -> Self + where + F: IntoIterator + Clone, + { + let unstable_manager = UnstableFeatureManager::new(features); let program_text = std::fs::read_to_string(prog_path).unwrap(); let source = CanonSourceFile::new( crate::source::CanonPath::canonicalize(prog_path).unwrap(), Arc::from(program_text), ); - let program = match TemplateProgram::new_with_dep( + let program = match TemplateProgram::with_unstable_and_dep( source, dependency_map, + &unstable_manager, Box::new(ElementsJetHinter::new()), ) { Ok(x) => x, @@ -464,8 +603,33 @@ pub(crate) mod tests { } pub fn template_text(program_text: Cow) -> Self { - let program = match TemplateProgram::new( + Self::template_text_with_unstable(program_text, std::iter::empty::()) + } + + pub fn template_text_with_unstable(program_text: Cow, features: F) -> Self + where + F: IntoIterator + Clone, + { + let unstable_manager = UnstableFeatureManager::new(features.clone()); + + let features_vec: Vec = features.into_iter().collect(); + if !features_vec.is_empty() { + let result = + TemplateProgram::new(program_text.as_ref(), Box::new(ElementsJetHinter::new())); + let err = result.expect_err("Program should fail without unstable flags"); + for feature in features_vec { + assert!( + err.contains(&feature.to_string()), + "Expected unstable feature error to mention '{}', got: {}", + feature, + err + ); + } + } + + let program = match TemplateProgram::with_unstable( program_text.as_ref(), + &unstable_manager, Box::new(ElementsJetHinter::new()), ) { Ok(x) => x, @@ -512,16 +676,59 @@ pub(crate) mod tests { .with_arguments(Arguments::default()) } + // Temporary allow while imports are unstable + #[allow(dead_code)] + pub fn program_file_with_unstable, F>( + program_file_path: P, + features: F, + ) -> Self + where + F: IntoIterator + Clone, + { + TestCase::::template_file_with_unstable(program_file_path, features) + .with_arguments(Arguments::default()) + } + pub fn program_text(program_text: Cow) -> Self { TestCase::::template_text(program_text) .with_arguments(Arguments::default()) } + // Temporary allow while imports are unstable + #[allow(dead_code)] + pub fn program_text_with_unstable(program_text: Cow, features: F) -> Self + where + F: IntoIterator + Clone, + { + TestCase::::template_text_with_unstable(program_text, features) + .with_arguments(Arguments::default()) + } + + // Temporary allow while imports are unstable + #[allow(dead_code)] pub fn program_file_with_deps(prog_path: P, dependencies: I) -> Self where P: AsRef, I: IntoIterator, K: Into, + { + Self::program_file_with_deps_and_unstable( + prog_path, + dependencies, + std::iter::empty::(), + ) + } + + pub fn program_file_with_deps_and_unstable( + prog_path: P, + dependencies: I, + features: F, + ) -> Self + where + P: AsRef, + I: IntoIterator, + K: Into, + F: IntoIterator + Clone, { let parent = prog_path.as_ref().parent().unwrap(); let canon_root = canon(parent); @@ -536,8 +743,36 @@ pub(crate) mod tests { let dependency_map = builder.build().unwrap(); - TestCase::::template_deps(prog_path.as_ref(), &dependency_map) - .with_arguments(Arguments::default()) + let features_vec: Vec = features.clone().into_iter().collect(); + if !features_vec.is_empty() { + let program_text = std::fs::read_to_string(prog_path.as_ref()).unwrap(); + let source = CanonSourceFile::new( + crate::source::CanonPath::canonicalize(prog_path.as_ref()).unwrap(), + Arc::from(program_text), + ); + let result = TemplateProgram::new_with_dep( + source, + &dependency_map, + Box::new(ElementsJetHinter::new()), + ); + let err = result + .expect_err("Program with dependencies should fail without unstable flags"); + for feature in features_vec { + assert!( + err.contains(&feature.to_string()), + "Expected unstable feature error to mention '{}', got: {}", + feature, + err + ); + } + } + + TestCase::::template_deps_with_unstable( + prog_path.as_ref(), + &dependency_map, + features, + ) + .with_arguments(Arguments::default()) } #[cfg(feature = "serde")] @@ -643,21 +878,31 @@ pub(crate) mod tests { /// THE DEFAULT HELPER /// Automatically sets up the standard `lib` self-referencing dependency. - pub(crate) fn run_dependency_test(root_path: &str, lib_alias: &str) { + pub(crate) fn run_dependency_test(root_path: &str, lib_alias: &str, features: F) + where + F: IntoIterator + Clone, + { let root_path = PathBuf::from(root_path); let lib_path = root_path.join(lib_alias); let main_path = root_path.join("main.simf"); - TestCase::program_file_with_deps(&main_path, [(&root_path, lib_alias, &lib_path)]) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::program_file_with_deps_and_unstable( + &main_path, + [(&root_path, lib_alias, &lib_path)], + features, + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } /// THE ADVANCED HELPER /// A helper function to run standard library dependency tests. /// `deps` expects an array of tuples: `(context_folder, alias, target_folder)`. /// Use `"."` for the `context_folder` if the context is the root test directory. - pub(crate) fn run_multidep_test(root_path: &str, deps: &[(&str, &str, &str)]) { + pub(crate) fn run_multidep_test(root_path: &str, deps: &[(&str, &str, &str)], features: F) + where + F: IntoIterator + Clone, + { let root_path = PathBuf::from(root_path); let main_path = root_path.join("main.simf"); @@ -679,7 +924,7 @@ pub(crate) mod tests { let ref_deps = mapped_deps.iter().map(|(c, a, t)| (c, *a, t)); - TestCase::program_file_with_deps(&main_path, ref_deps) + TestCase::program_file_with_deps_and_unstable(&main_path, ref_deps, features) .with_witness_values(WitnessValues::default()) .assert_run_success(); } @@ -692,7 +937,15 @@ pub(crate) mod tests { /// ``` #[test] fn single_dep() { - run_dependency_test("./examples/single_dep", "temp"); + run_dependency_test( + "./examples/single_dep", + "temp", + [ + UnstableFeature::UseKeyword, + UnstableFeature::CrateKeyword, + UnstableFeature::AsKeyword, + ], + ); } /// Run with `simc` command: @@ -707,6 +960,7 @@ pub(crate) mod tests { run_multidep_test( "./examples/simple_multidep", &[(".", "math", "math"), (".", "crypto", "crypto")], + [UnstableFeature::UseKeyword], ); } @@ -727,6 +981,7 @@ pub(crate) mod tests { (".", "base_math", "math"), ("merkle", "math", "math"), ], + [UnstableFeature::UseKeyword, UnstableFeature::AsKeyword], ); } @@ -737,7 +992,11 @@ pub(crate) mod tests { /// ``` #[test] fn local_crate() { - run_multidep_test("./examples/local_crate", &[]); + run_multidep_test( + "./examples/local_crate", + &[], + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], + ); } #[test] @@ -758,10 +1017,14 @@ pub(crate) mod tests { let dependency_map = DependencyMapBuilder::new(canon_root).build().unwrap(); - TestCase::::template_deps(&main_path, &dependency_map) - .with_arguments(Arguments::default()) - .with_witness_values(WitnessValues::default()) - .assert_run_success(); + TestCase::::template_deps_with_unstable( + &main_path, + &dependency_map, + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], + ) + .with_arguments(Arguments::default()) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); } #[test] @@ -1217,10 +1480,12 @@ mod error_tests { ); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); + let unstable_manager = UnstableFeatureManager::new(UnstableFeature::all().iter().copied()); - let err = TemplateProgram::new_with_dep( + let err = TemplateProgram::with_unstable_and_dep( source_file(&main_path), &dependencies, + &unstable_manager, Box::new(ElementsJetHinter::new()), ) .expect_err("dependency body has a type error"); @@ -1248,9 +1513,11 @@ mod error_tests { ws.create_file("workspace/lib/base.simf", "pub fn one() -> u32 { 1 }\n"); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); - let _err = TemplateProgram::new_with_dep( + let unstable_manager = UnstableFeatureManager::new(UnstableFeature::all().iter().copied()); + let _err = TemplateProgram::with_unstable_and_dep( source_file(&main_path), &dependencies, + &unstable_manager, Box::new(ElementsJetHinter::new()), ) .expect_err("omitted-context dependencies"); @@ -1266,10 +1533,12 @@ mod error_tests { "use lib::missing::Thing;\nfn main() {}\n", ); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); + let unstable_manager = UnstableFeatureManager::new(UnstableFeature::all().iter().copied()); - let err = TemplateProgram::new_with_dep( + let err = TemplateProgram::with_unstable_and_dep( source_file(&main_path), &dependencies, + &unstable_manager, Box::new(ElementsJetHinter::new()), ) .expect_err("missing imported module should fail"); @@ -1283,7 +1552,10 @@ mod error_tests { #[cfg(test)] mod functional_tests { - use crate::tests::{run_dependency_test, run_multidep_test}; + use crate::{ + tests::{run_dependency_test, run_multidep_test}, + UnstableFeature, + }; const VALID_TESTS_DIR: &str = "./functional-tests/valid-test-cases"; const ERROR_TESTS_DIR: &str = "./functional-tests/error-test-cases"; @@ -1291,7 +1563,11 @@ mod functional_tests { // Real test cases #[test] fn module_simple() { - run_dependency_test(format!("{}/module-simple", VALID_TESTS_DIR).as_str(), "lib"); + run_dependency_test( + format!("{}/module-simple", VALID_TESTS_DIR).as_str(), + "lib", + [UnstableFeature::UseKeyword], + ); } #[test] @@ -1299,6 +1575,7 @@ mod functional_tests { run_dependency_test( format!("{}/diamond-dependency-resolution", VALID_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1307,6 +1584,7 @@ mod functional_tests { run_dependency_test( format!("{}/deep-reexport-chain", VALID_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1315,6 +1593,7 @@ mod functional_tests { run_dependency_test( format!("{}/leaky-signature", VALID_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword], ); } @@ -1323,6 +1602,7 @@ mod functional_tests { run_dependency_test( format!("{}/reexport-diamond", VALID_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1336,6 +1616,7 @@ mod functional_tests { ("api", "crypto", "crypto"), ("api", "math", "math"), ], + [UnstableFeature::UseKeyword], ); } @@ -1352,6 +1633,7 @@ mod functional_tests { ("auth", "types", "types"), ("auth", "db", "db"), ], + [UnstableFeature::UseKeyword], ); } @@ -1362,6 +1644,7 @@ mod functional_tests { run_dependency_test( format!("{}/cyclic-dependency", ERROR_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1371,13 +1654,18 @@ mod functional_tests { run_dependency_test( format!("{}/file-not-found", ERROR_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword], ); } #[test] #[should_panic(expected = "DependencyPathNotFound")] fn lib_not_found_error() { - run_dependency_test(format!("{}/lib-not-found", ERROR_TESTS_DIR).as_str(), "lib"); + run_dependency_test( + format!("{}/lib-not-found", ERROR_TESTS_DIR).as_str(), + "lib", + [UnstableFeature::UseKeyword], + ); } #[test] @@ -1386,6 +1674,7 @@ mod functional_tests { run_dependency_test( format!("{}/private-visibility", ERROR_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword], ); } @@ -1395,6 +1684,7 @@ mod functional_tests { run_dependency_test( format!("{}/name-collision", ERROR_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword], ); } @@ -1405,12 +1695,17 @@ mod functional_tests { run_dependency_test( format!("{}/type-alias-duplication", ERROR_TESTS_DIR).as_str(), "lib", + [UnstableFeature::UseKeyword], ); } #[test] fn local_crate_resolution() { - run_multidep_test(format!("{}/local-crate", VALID_TESTS_DIR).as_str(), &[]); + run_multidep_test( + format!("{}/local-crate", VALID_TESTS_DIR).as_str(), + &[], + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], + ); } #[test] @@ -1418,6 +1713,7 @@ mod functional_tests { run_multidep_test( format!("{}/local-crate-nested", VALID_TESTS_DIR).as_str(), &[], + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1426,6 +1722,7 @@ mod functional_tests { run_multidep_test( format!("{}/external-library-uses-crate", VALID_TESTS_DIR).as_str(), &[(".", "ext_lib", "ext_lib")], + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1435,6 +1732,7 @@ mod functional_tests { run_multidep_test( format!("{}/crate-file-not-found", ERROR_TESTS_DIR).as_str(), &[], + [UnstableFeature::UseKeyword, UnstableFeature::CrateKeyword], ); } @@ -1446,6 +1744,7 @@ mod functional_tests { run_multidep_test( format!("{}/local-file-as-external", ERROR_TESTS_DIR).as_str(), &[(".", "ext", ".")], + [UnstableFeature::UseKeyword], ); } } diff --git a/src/main.rs b/src/main.rs index 5b29aa24..165728fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use clap::{Arg, ArgAction, Command}; use simplicityhl::ast::ElementsJetHinter; use simplicityhl::{ resolution::DependencyMapBuilder, source::CanonPath, source::CanonSourceFile, AbiMeta, - CompiledProgram, + CompiledProgram, UnstableFeatureManager, }; use std::path::Path; use std::{env, fmt}; @@ -38,6 +38,22 @@ impl fmt::Display for Output { } fn main() -> Result<(), Box> { + let max_len = simplicityhl::UnstableFeature::all() + .iter() + .map(|f| f.to_string().len()) + .max() + .unwrap_or(0); + + let mut unstable_help = String::from("Enable unstable features. Available features:\n"); + for feature in simplicityhl::UnstableFeature::all() { + unstable_help.push_str(&format!( + " {name: Result<(), Box> { .action(ArgAction::SetTrue) .help("Additional ABI .simf contract types"), ) + .arg( + Arg::new("unstable_features") + .long("unstable-feature") + .short('Z') + .value_name("FEATURE") + .action(ArgAction::Append) + .help(unstable_help), + ) }; let matches = command.get_matches(); @@ -106,6 +130,18 @@ fn main() -> Result<(), Box> { let output_json = matches.get_flag("json"); let abi_param = matches.get_flag("abi"); + // Parse unstable features + let unstable_manager = if let Some(features) = matches.get_many::("unstable_features") { + UnstableFeatureManager::from_feature_names(features.map(|s| s.as_str())).unwrap_or_else( + |e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }, + ) + } else { + UnstableFeatureManager::default() + }; + #[cfg(feature = "serde")] let args_opt: simplicityhl::Arguments = match matches.get_one::("args_file") { None => simplicityhl::Arguments::default(), @@ -170,9 +206,10 @@ fn main() -> Result<(), Box> { }; let source = CanonSourceFile::new(main_path.clone(), std::sync::Arc::from(main_text)); - let compiled = match CompiledProgram::new_with_dep( + let compiled = match CompiledProgram::with_unstable_and_dep( source, &dependencies, + &unstable_manager, args_opt, include_debug_symbols, Box::new(ElementsJetHinter::new()), diff --git a/src/unstable.rs b/src/unstable.rs new file mode 100644 index 00000000..b4c73989 --- /dev/null +++ b/src/unstable.rs @@ -0,0 +1,265 @@ +//! Unstable feature management for SimplicityHL compiler. + +use std::collections::HashSet; +use std::fmt; +use std::str::FromStr; + +use crate::error::{Error, ErrorCollector, RichError}; +use crate::parse::{Item, Program, UseItems}; + +/// Keeps track of unstable features available in the compiler. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum UnstableFeature { + /// The `use` keyword for including multiple source files. + UseKeyword, + /// The `crate` keyword for referencing the local project root. + CrateKeyword, + /// The `as` keyword for aliasing imports. + AsKeyword, +} + +impl fmt::Display for UnstableFeature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UseKeyword => write!(f, "use-keyword"), + Self::CrateKeyword => write!(f, "crate-keyword"), + Self::AsKeyword => write!(f, "as-keyword"), + } + } +} + +impl FromStr for UnstableFeature { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "use-keyword" => Ok(UnstableFeature::UseKeyword), + "crate-keyword" => Ok(UnstableFeature::CrateKeyword), + "as-keyword" => Ok(UnstableFeature::AsKeyword), + _ => Err(format!("Unknown unstable feature: '{}'", s)), + } + } +} + +impl UnstableFeature { + pub fn description(&self) -> &'static str { + match self { + Self::UseKeyword => "The 'use' keyword for using dependencies", + Self::CrateKeyword => "The 'crate' keyword for referencing the local project root", + Self::AsKeyword => "The 'as' keyword for aliasing imports", + } + } + + pub fn all() -> &'static [UnstableFeature] { + &[Self::UseKeyword, Self::CrateKeyword, Self::AsKeyword] + } +} + +/// Manages the state of unstable features during compilation. +/// This struct tracks which unstable features are enabled and provides +/// validation methods for checking feature availability. +/// In default mode, all features are disabled. Features can be enabled via command-line flags +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct UnstableFeatureManager { + enabled_features: HashSet, +} + +impl UnstableFeatureManager { + /// Creates a manager with specific features enabled. + pub fn new(features: impl IntoIterator) -> Self { + Self { + enabled_features: features.into_iter().collect(), + } + } + + pub fn is_enabled(&self, feature: UnstableFeature) -> bool { + self.enabled_features.contains(&feature) + } + + pub fn check_feature(&self, feature: UnstableFeature) -> Result<(), Error> { + if !self.is_enabled(feature) { + return Err(Error::UnstableFeature { + feature_name: feature.to_string(), + }); + } + Ok(()) + } + + pub fn check_program(&self, program: &Program, handler: &mut ErrorCollector) { + for item in program.items() { + let Item::Use(use_decl) = item else { + continue; + }; + + let mut check_and_report = |feature| { + if let Err(error) = self.check_feature(feature) { + handler.push(RichError::new(error, *use_decl.span())); + } + }; + + check_and_report(UnstableFeature::UseKeyword); + + if use_decl + .drp_name() + .is_ok_and(|drp| drp == crate::driver::CRATE_STR) + { + check_and_report(UnstableFeature::CrateKeyword); + } + + let has_alias = match use_decl.items() { + UseItems::Single((_, alias)) => alias.is_some(), + UseItems::List(items) => items.iter().any(|(_, alias)| alias.is_some()), + }; + + if has_alias { + check_and_report(UnstableFeature::AsKeyword); + } + } + } + + pub fn from_feature_names( + names: impl IntoIterator>, + ) -> Result { + let mut features = HashSet::new(); + + for name in names { + let feature_name = name.as_ref().trim(); + if !feature_name.is_empty() { + features.insert(feature_name.parse::()?); + } + } + + Ok(Self { + enabled_features: features, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_feature_display() { + for feature in UnstableFeature::all() { + let name = feature.to_string(); + assert!(!name.is_empty()); + assert!(!name.contains(' ')); + } + } + + #[test] + fn test_feature_descriptions() { + for feature in UnstableFeature::all() { + assert!(!feature.description().is_empty()); + } + } + + #[test] + fn test_all_features() { + let all_features = UnstableFeature::all(); + let mut unique = HashSet::new(); + for feature in all_features { + assert!( + unique.insert(*feature), + "Features in all() should be unique" + ); + } + } + + #[test] + fn test_feature_from_str() { + for feature in UnstableFeature::all() { + let parsed = feature + .to_string() + .parse::() + .expect("Should parse from string"); + assert_eq!(*feature, parsed); + } + } + + #[test] + fn test_default_manager() { + let manager = UnstableFeatureManager::default(); + for feature in UnstableFeature::all() { + assert!(!manager.is_enabled(*feature)); + } + } + + #[test] + fn test_new_single() { + let Some(&feature) = UnstableFeature::all().first() else { + return; + }; + let manager = UnstableFeatureManager::new([feature]); + assert!(manager.is_enabled(feature)); + } + + #[test] + fn test_check_feature_enabled() { + let Some(&feature) = UnstableFeature::all().first() else { + return; + }; + let manager = UnstableFeatureManager::new([feature]); + assert!(manager.check_feature(feature).is_ok()); + } + + #[test] + fn test_check_feature_disabled() { + let manager = UnstableFeatureManager::default(); + let Some(&feature) = UnstableFeature::all().first() else { + return; + }; + let error = manager.check_feature(feature).unwrap_err().to_string(); + assert!(error.contains(&feature.to_string())); + assert!(error.contains("not enabled")); + assert!(error.contains("-Z")); + } + + #[test] + fn test_from_feature_names_single() { + let Some(&feature) = UnstableFeature::all().first() else { + return; + }; + let manager = + UnstableFeatureManager::from_feature_names(vec![feature.to_string()]).unwrap(); + assert!(manager.is_enabled(feature)); + } + + #[test] + fn test_from_feature_names_multiple() { + let names: Vec<_> = UnstableFeature::all() + .iter() + .map(|f| f.to_string()) + .collect(); + let manager = UnstableFeatureManager::from_feature_names(names).unwrap(); + for feature in UnstableFeature::all() { + assert!(manager.is_enabled(*feature)); + } + } + + #[test] + fn test_from_feature_names_empty() { + let manager = UnstableFeatureManager::from_feature_names(Vec::<&str>::new()).unwrap(); + for feature in UnstableFeature::all() { + assert!(!manager.is_enabled(*feature)); + } + } + + #[test] + fn test_from_feature_names_with_whitespace() { + let Some(&feature) = UnstableFeature::all().first() else { + return; + }; + let manager = + UnstableFeatureManager::from_feature_names(vec![format!(" {} ", feature)]).unwrap(); + assert!(manager.is_enabled(feature)); + } + + #[test] + fn test_from_feature_names_unknown() { + let result = UnstableFeatureManager::from_feature_names(vec!["unknown-feature"]); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Unknown")); + } +} diff --git a/tests/cli.rs b/tests/cli.rs index 6ede40b4..6099d2c5 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -14,6 +14,10 @@ fn cli_dependency_can_use_crate_root() { let output = Command::new(env!("CARGO_BIN_EXE_simc")) .arg(main) + .arg("-Z") + .arg("use-keyword") + .arg("-Z") + .arg("crate-keyword") .arg("--dep") .arg(dep_arg) .output()