浏览代码

Allow repeated includes

David Peter 2 年之前
父节点
当前提交
9a77db880f

+ 7 - 0
Cargo.lock

@@ -497,6 +497,12 @@ version = "0.27.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "h2"
 version = "0.3.20"
@@ -864,6 +870,7 @@ version = "0.1.0"
 dependencies = [
  "approx",
  "codespan-reporting",
+ "glob",
  "itertools",
  "num-rational",
  "num-traits",

+ 1 - 1
examples/name_resolution_error/alias_clash.nbt

@@ -4,4 +4,4 @@ dimension Foo
 unit foo: Length
 
 @aliases(kilofoo)
-unit bar: Length
+unit baz: Length

+ 3 - 0
modules/math/constants.nbt

@@ -1,3 +1,6 @@
+use core::scalar
+use math::functions
+
 ### Mathematical constants
 
 let pi = 3.14159265358979323846264338327950288

+ 2 - 0
modules/math/functions.nbt

@@ -1,3 +1,5 @@
+use core::scalar
+
 ### Mathematical functions
 
 fn abs<T>(x: T) -> T

+ 2 - 0
modules/physics/constants.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Physics constants
 
 # The speed of light in vacuum

+ 2 - 0
modules/physics/temperature_conversion.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Temperature conversion functions K <-> °C and K <-> °F
 
 let offset_celsius = 273.15

+ 2 - 0
modules/units/astronomical.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 @metric_prefixes
 @aliases(parsecs, pc: short)
 unit parsec: Length = 648000 / π × au

+ 2 - 0
modules/units/bit.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 dimension Bit
 
 @metric_prefixes

+ 2 - 0
modules/units/cgs.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Centimetre–gram–second system of units
 
 @aliases(dyn)

+ 2 - 0
modules/units/fff.nbt

@@ -1,3 +1,5 @@
+use units::imperial
+
 # The furlong–firkin–fortnight system
 # https://en.wikipedia.org/wiki/FFF_system
 

+ 4 - 0
modules/units/imperial.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Imperial unit system
 
 @aliases(inches, in: short)
@@ -26,3 +28,5 @@ unit thou: Length = 0.0000254 meter
 
 @aliases(fathoms)
 unit fathom: Length = 2 yard
+
+unit mph = miles per hour

+ 2 - 1
modules/units/misc.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Other units
 
 @metric_prefixes
@@ -49,5 +51,4 @@ unit molal: AmountOfSubstance / Mass = 1 mole / kilogram
 @aliases(Wh: short)
 unit watthour = W h
 
-unit mph = miles per hour
 unit kph = kilometer per hour

+ 2 - 0
modules/units/nautical.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 @aliases(knots, kn: short, kt: short)
 unit knot: Speed = 463 m / 900 s
 

+ 3 - 0
modules/units/non_euro_currencies.nbt

@@ -1,3 +1,6 @@
+use core::scalar
+use units::currency
+
 # This module is currently not part of the prelude, because the 'exchange_rate_XXX' calls
 # are blocking. In the CLI application, we do however load this module asynchronously after
 # prefetching the exchange rates.

+ 2 - 0
modules/units/placeholder.nbt

@@ -1,3 +1,5 @@
+use units::imperial
+
 # Smallest addressable element on a digital display
 dimension Pixel
 

+ 3 - 0
modules/units/si.nbt

@@ -1,3 +1,6 @@
+use core::dimensions
+use math::constants
+
 ### SI base units
 
 @metric_prefixes

+ 2 - 0
modules/units/time.nbt

@@ -1,3 +1,5 @@
+use units::si
+
 ### Time units
 
 @aliases(weeks)

+ 3 - 0
modules/units/us_customary.nbt

@@ -1,3 +1,6 @@
+use units::si
+use units::imperial
+
 # US liquid gallon
 @aliases(gallons, gal: short)
 unit gallon: Volume = 0.003785411784 meter^3

+ 9 - 3
numbat-cli/src/main.rs

@@ -9,7 +9,7 @@ use highlighter::NumbatHighlighter;
 use numbat::diagnostic::ErrorDiagnostic;
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::{CodeSource, FileSystemImporter};
-use numbat::{markup, NameResolutionError};
+use numbat::{markup, NameResolutionError, RuntimeError};
 use numbat::{Context, ExitStatus, InterpreterResult, NumbatError};
 
 use anyhow::{bail, Context as AnyhowContext, Result};
@@ -359,8 +359,14 @@ impl Cli {
                 execution_mode.exit_status_in_case_of_error()
             }
             Err(NumbatError::RuntimeError(e)) => {
-                self.print_diagnostic(e);
-                execution_mode.exit_status_in_case_of_error()
+                if execution_mode == ExecutionMode::Interactive
+                    && matches!(e, RuntimeError::NoStatements)
+                {
+                    ControlFlow::Continue(())
+                } else {
+                    self.print_diagnostic(e);
+                    execution_mode.exit_status_in_case_of_error()
+                }
             }
         }
     }

+ 1 - 0
numbat/Cargo.toml

@@ -16,3 +16,4 @@ numbat-exchange-rates = { path = "../numbat-exchange-rates" }
 
 [dev-dependencies]
 approx = "0.5"
+glob = "0.3"

+ 2 - 1
numbat/src/lib.rs

@@ -30,7 +30,7 @@ mod vm;
 use bytecode_interpreter::BytecodeInterpreter;
 use currency::ExchangeRatesCache;
 use diagnostic::ErrorDiagnostic;
-use interpreter::{Interpreter, RuntimeError};
+use interpreter::Interpreter;
 use prefix_transformer::Transformer;
 use resolver::CodeSource;
 use resolver::ModuleImporter;
@@ -44,6 +44,7 @@ use ast::Statement;
 pub use diagnostic::Diagnostic;
 pub use interpreter::ExitStatus;
 pub use interpreter::InterpreterResult;
+pub use interpreter::RuntimeError;
 pub use name_resolution::NameResolutionError;
 pub use parser::ParseError;
 

+ 45 - 10
numbat/src/resolver.rs

@@ -44,6 +44,7 @@ pub(crate) struct Resolver {
     importer: Box<dyn ModuleImporter + Send>,
     code_sources: Vec<CodeSource>,
     pub files: SimpleFiles<String, String>,
+    imported_modules: Vec<ModulePath>,
 }
 
 impl Resolver {
@@ -52,6 +53,7 @@ impl Resolver {
             importer: Box::new(importer),
             code_sources: vec![],
             files: SimpleFiles::new(),
+            imported_modules: vec![],
         }
     }
 
@@ -86,17 +88,20 @@ impl Resolver {
         for statement in program {
             match statement {
                 Statement::ModuleImport(span, module_path) => {
-                    if let Some((code, filesystem_path)) = self.importer.import(module_path) {
-                        let index = self.add_code_source(
-                            CodeSource::Module(module_path.clone(), filesystem_path),
-                            &code,
-                        );
-                        for statement in self.parse(&code, index)? {
-                            new_program.push(statement);
+                    if !self.imported_modules.contains(module_path) {
+                        self.imported_modules.push(module_path.clone());
+                        if let Some((code, filesystem_path)) = self.importer.import(module_path) {
+                            let index = self.add_code_source(
+                                CodeSource::Module(module_path.clone(), filesystem_path),
+                                &code,
+                            );
+                            for statement in self.parse(&code, index)? {
+                                new_program.push(statement);
+                            }
+                            performed_imports = true;
+                        } else {
+                            return Err(ResolverError::UnknownModule(*span, module_path.clone()));
                         }
-                        performed_imports = true;
-                    } else {
-                        return Err(ResolverError::UnknownModule(*span, module_path.clone()));
                     }
                 }
                 statement => new_program.push(statement.clone()),
@@ -214,4 +219,34 @@ mod tests {
             ]
         );
     }
+
+    #[test]
+    fn resolver_repeated_includes() {
+        use crate::ast::ReplaceSpans;
+
+        let program = "
+        use foo::bar
+        use foo::bar
+        a
+        ";
+
+        let importer = TestImporter {};
+
+        let mut resolver = Resolver::new(importer);
+        let program_inlined = resolver.resolve(program, CodeSource::Text).unwrap();
+
+        assert_eq!(
+            &program_inlined.replace_spans(),
+            &[
+                Statement::DeclareVariable {
+                    identifier_span: Span::dummy(),
+                    identifier: "a".into(),
+                    expr: Expression::Scalar(Span::dummy(), Number::from_f64(1.0)),
+                    type_annotation_span: None,
+                    type_annotation: None
+                },
+                Statement::Expression(Expression::Identifier(Span::dummy(), "a".into()))
+            ]
+        );
+    }
 }

+ 7 - 6
numbat/tests/common.rs

@@ -5,19 +5,20 @@ use numbat::{
     Context,
 };
 
-pub fn get_test_context() -> Context {
+pub fn get_test_context_without_prelude() -> Context {
     let module_path = Path::new("../modules");
 
     let mut importer = FileSystemImporter::default();
     importer.add_path(module_path);
 
-    let mut context = Context::new(importer);
+    Context::new(importer)
+}
+
+pub fn get_test_context() -> Context {
+    let mut context = get_test_context_without_prelude();
 
     assert!(context
-        .interpret(
-            &std::fs::read_to_string(module_path.join("prelude.nbt")).unwrap(),
-            CodeSource::Text
-        )
+        .interpret("use prelude", CodeSource::Text)
         .expect("Error while running prelude")
         .1
         .is_success());

+ 26 - 10
numbat/tests/prelude_and_examples.rs

@@ -8,7 +8,9 @@ use numbat::{InterpreterResult, NumbatError};
 use std::ffi::OsStr;
 use std::fs;
 
-fn assert_typechecks_and_runs(code: &str) {
+use crate::common::get_test_context_without_prelude;
+
+fn assert_runs(code: &str) {
     let result = get_test_context().interpret(code, CodeSource::Text);
     assert!(result.is_ok());
     assert!(matches!(
@@ -17,6 +19,15 @@ fn assert_typechecks_and_runs(code: &str) {
     ));
 }
 
+fn assert_runs_without_prelude(code: &str) {
+    let result = get_test_context_without_prelude().interpret(code, CodeSource::Text);
+    assert!(result.is_ok());
+    assert!(matches!(
+        result.unwrap().1,
+        InterpreterResult::Quantity(_) | InterpreterResult::Continue
+    ));
+}
+
 fn assert_parse_error(code: &str) {
     assert!(matches!(
         get_test_context().interpret(code, CodeSource::Text),
@@ -45,9 +56,9 @@ fn assert_runtime_error(code: &str) {
     ));
 }
 
-fn run_for_each_numbat_file_in(folder: &str, f: impl Fn(&str)) {
-    for entry in fs::read_dir(folder).unwrap() {
-        let path = entry.unwrap().path();
+fn run_for_each_file(glob_pattern: &str, f: impl Fn(&str)) {
+    for entry in glob::glob(glob_pattern).unwrap() {
+        let path = entry.unwrap();
         if path.extension() != Some(OsStr::new("nbt")) {
             continue;
         }
@@ -59,30 +70,35 @@ fn run_for_each_numbat_file_in(folder: &str, f: impl Fn(&str)) {
     }
 }
 
+#[test]
+fn modules_are_self_consistent() {
+    run_for_each_file("../modules/**/*.nbt", assert_runs_without_prelude);
+}
+
 #[test]
 fn examples_can_be_parsed_and_interpreted() {
-    run_for_each_numbat_file_in("../examples/", assert_typechecks_and_runs);
+    run_for_each_file("../examples/*.nbt", assert_runs);
 }
 
 #[test]
 fn parse_error_examples_fail_as_expected() {
-    run_for_each_numbat_file_in("../examples/parse_error", assert_parse_error);
+    run_for_each_file("../examples/parse_error/*.nbt", assert_parse_error);
 }
 
 #[test]
 fn name_resolution_error_examples_fail_as_expected() {
-    run_for_each_numbat_file_in(
-        "../examples/name_resolution_error",
+    run_for_each_file(
+        "../examples/name_resolution_error/*.nbt",
         assert_name_resolution_error,
     );
 }
 
 #[test]
 fn typecheck_error_examples_fail_as_expected() {
-    run_for_each_numbat_file_in("../examples/typecheck_error", assert_typecheck_error);
+    run_for_each_file("../examples/typecheck_error/*.nbt", assert_typecheck_error);
 }
 
 #[test]
 fn runtime_error_examples_fail_as_expected() {
-    run_for_each_numbat_file_in("../examples/runtime_error", assert_runtime_error);
+    run_for_each_file("../examples/runtime_error/*.nbt", assert_runtime_error);
 }