Browse Source

Initial support for module imports

David Peter 2 years ago
parent
commit
cb2645a1d4

+ 4 - 0
numbat-cli/src/main.rs

@@ -251,6 +251,10 @@ impl Cli {
 
                 execution_mode.exit_status_in_case_of_error()
             }
+            Err(NumbatError::ResolverError(e)) => {
+                eprintln!("Module resolver error: {:#}", e);
+                execution_mode.exit_status_in_case_of_error()
+            }
             Err(NumbatError::NameResolutionError(e)) => {
                 eprintln!("Name resolution error: {:#}", e);
                 execution_mode.exit_status_in_case_of_error()

+ 12 - 2
numbat/src/ast.rs

@@ -2,7 +2,7 @@ use crate::markup as m;
 use crate::prefix_parser::AcceptsPrefix;
 use crate::{
     arithmetic::Exponent, decorator::Decorator, markup::Markup, number::Number, prefix::Prefix,
-    pretty_print::PrettyPrint,
+    pretty_print::PrettyPrint, resolver::ModulePath,
 };
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -294,6 +294,7 @@ pub enum Statement {
         Vec<Decorator>,
     ),
     ProcedureCall(ProcedureKind, Vec<Expression>),
+    ModuleImport(ModulePath),
 }
 
 fn accepts_prefix_markup(accepts_prefix: &Option<AcceptsPrefix>) -> Markup {
@@ -470,6 +471,15 @@ impl PrettyPrint for Statement {
                     .sum()
                     + m::operator(")")
             }
+            Statement::ModuleImport(module_path) => {
+                m::keyword("use")
+                    + m::space()
+                    + Itertools::intersperse(
+                        module_path.0.iter().map(|m| m::identifier(m)),
+                        m::operator("::"),
+                    )
+                    .sum()
+            }
         }
     }
 }
@@ -535,7 +545,7 @@ mod tests {
             )
             .unwrap();
 
-        let statements = crate::parse(code).unwrap();
+        let statements = crate::parser::parse(code).unwrap();
         transformer.transform(statements).unwrap()[0].clone()
     }
 

+ 1 - 11
numbat/src/dimension.rs

@@ -61,20 +61,10 @@ impl DimensionRegistry {
     }
 }
 
-#[cfg(test)]
-pub fn parse_dexpr(input: &str) -> DimensionExpression {
-    let tokens = crate::tokenizer::tokenize(input).expect("No tokenizer errors in tests");
-    let mut parser = crate::parser::Parser::new(&tokens);
-    let expr = parser
-        .dimension_expression()
-        .expect("No parser errors in tests");
-    assert!(parser.is_at_end());
-    expr
-}
-
 #[test]
 fn basic() {
     use crate::arithmetic::Rational;
+    use crate::parser::parse_dexpr;
     use crate::registry::BaseRepresentationFactor;
 
     let mut registry = DimensionRegistry::default();

+ 13 - 2
numbat/src/lib.rs

@@ -16,6 +16,7 @@ pub mod pretty_print;
 mod product;
 mod quantity;
 mod registry;
+mod resolver;
 mod span;
 mod tokenizer;
 mod typechecker;
@@ -27,8 +28,10 @@ mod vm;
 use bytecode_interpreter::BytecodeInterpreter;
 use interpreter::{Interpreter, RuntimeError};
 use name_resolution::NameResolutionError;
-use parser::parse;
 use prefix_transformer::Transformer;
+use resolver::NullImporter;
+use resolver::Resolver;
+use resolver::ResolverError;
 use thiserror::Error;
 use typechecker::{TypeCheckError, TypeChecker};
 
@@ -42,6 +45,8 @@ pub enum NumbatError {
     #[error("{0}")]
     ParseError(ParseError),
     #[error("{0}")]
+    ResolverError(ResolverError),
+    #[error("{0}")]
     NameResolutionError(NameResolutionError),
     #[error("{0}")]
     TypeCheckError(TypeCheckError),
@@ -77,7 +82,13 @@ impl Context {
     }
 
     pub fn interpret(&mut self, code: &str) -> Result<(Vec<Statement>, InterpreterResult)> {
-        let statements = parse(code).map_err(NumbatError::ParseError)?;
+        let importer = NullImporter {};
+        let resolver = Resolver::new(&importer);
+
+        let statements = resolver.resolve(code).map_err(|e| match e {
+            ResolverError::ParseError(e) => NumbatError::ParseError(e),
+            e => NumbatError::ResolverError(e),
+        })?;
 
         let prefix_transformer_old = self.prefix_transformer.clone();
 

+ 29 - 2
numbat/src/parser.rs

@@ -12,7 +12,7 @@
 //!
 //! Grammar:
 //! ```txt
-//! statement       →   expression | variable_decl | function_decl | dimension_decl | unit_decl | procedure_call
+//! statement       →   expression | variable_decl | function_decl | dimension_decl | unit_decl | procedure_call | module_import
 //!
 //! variable_decl   →   …
 //! function_decl   →   …
@@ -38,6 +38,7 @@ use crate::ast::{BinaryOperator, DimensionExpression, Expression, ProcedureKind,
 use crate::decorator::Decorator;
 use crate::number::Number;
 use crate::prefix_parser::AcceptsPrefix;
+use crate::resolver::ModulePath;
 use crate::span::Span;
 use crate::tokenizer::{Token, TokenKind, TokenizerError, TokenizerErrorKind};
 
@@ -131,7 +132,7 @@ impl ParseError {
 
 type Result<T> = std::result::Result<T, ParseError>;
 
-pub struct Parser<'a> {
+struct Parser<'a> {
     tokens: &'a [Token],
     current: usize,
     decorator_stack: Vec<Decorator>,
@@ -445,6 +446,21 @@ impl<'a> Parser<'a> {
                     span: self.peek().span.clone(),
                 })
             }
+        } else if self.match_exact(TokenKind::Use).is_some() {
+            if let Some(identifier) = self.match_exact(TokenKind::Identifier) {
+                let mut module_path = vec![identifier.lexeme.clone()];
+
+                while self.match_exact(TokenKind::ColonColon).is_some() {
+                    if let Some(identifier) = self.match_exact(TokenKind::Identifier) {
+                        module_path.push(identifier.lexeme.clone());
+                    } else {
+                        todo!("Parse error")
+                    }
+                }
+                Ok(Statement::ModuleImport(ModulePath(module_path)))
+            } else {
+                todo!("Parse error")
+            }
         } else if self
             .match_any(&[TokenKind::ProcedurePrint, TokenKind::ProcedureAssertEq])
             .is_some()
@@ -849,6 +865,17 @@ pub fn parse(input: &str) -> Result<Vec<Statement>> {
     parser.parse()
 }
 
+#[cfg(test)]
+pub fn parse_dexpr(input: &str) -> DimensionExpression {
+    let tokens = crate::tokenizer::tokenize(input).expect("No tokenizer errors in tests");
+    let mut parser = crate::parser::Parser::new(&tokens);
+    let expr = parser
+        .dimension_expression()
+        .expect("No parser errors in tests");
+    assert!(parser.is_at_end());
+    expr
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

+ 1 - 0
numbat/src/prefix_transformer.rs

@@ -113,6 +113,7 @@ impl Transformer {
                     .map(|arg| self.transform_expression(arg))
                     .collect(),
             ),
+            statement @ Statement::ModuleImport(_) => statement,
         })
     }
 

+ 133 - 0
numbat/src/resolver.rs

@@ -0,0 +1,133 @@
+use crate::{ast::Statement, parser::parse, ParseError};
+
+use thiserror::Error;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ModulePath(pub Vec<String>);
+
+impl std::fmt::Display for ModulePath {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", itertools::join(self.0.iter().cloned(), "::"))
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum ResolverError {
+    #[error("Unknown module '{0}'.")]
+    UnknownModule(ModulePath),
+
+    #[error("{0}")]
+    ParseError(ParseError),
+}
+
+type Result<T> = std::result::Result<T, ResolverError>;
+
+pub(crate) struct Resolver<'a> {
+    importer: &'a dyn ModuleImporter,
+}
+
+impl<'a> Resolver<'a> {
+    pub(crate) fn new(importer: &'a dyn ModuleImporter) -> Self {
+        Self { importer }
+    }
+
+    fn parse(&self, code: &str) -> Result<Vec<Statement>> {
+        parse(code).map_err(ResolverError::ParseError)
+    }
+
+    fn inlining_pass(&self, program: &[Statement]) -> Result<(Vec<Statement>, bool)> {
+        let mut new_program = vec![];
+        let mut performed_imports = false;
+
+        for statement in program {
+            match statement {
+                Statement::ModuleImport(module_path) => {
+                    if let Some(code) = self.importer.import(module_path) {
+                        for statement in parse(&code).unwrap() {
+                            new_program.push(statement);
+                        }
+                        performed_imports = true;
+                    } else {
+                        return Err(ResolverError::UnknownModule(module_path.clone()));
+                    }
+                }
+                statement => new_program.push(statement.clone()),
+            }
+        }
+
+        Ok((new_program, performed_imports))
+    }
+
+    pub fn resolve(&self, code: &str) -> Result<Vec<Statement>> {
+        // TODO: handle cyclic dependencies & infinite loops
+
+        let mut statements = self.parse(code)?;
+
+        loop {
+            let result = self.inlining_pass(&statements)?;
+            statements = result.0;
+            if !result.1 {
+                return Ok(statements);
+            }
+        }
+    }
+}
+
+pub trait ModuleImporter {
+    fn import(&self, path: &ModulePath) -> Option<String>;
+}
+
+pub struct NullImporter {}
+
+impl ModuleImporter for NullImporter {
+    fn import(&self, _: &ModulePath) -> Option<String> {
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        ast::{Expression, Statement},
+        number::Number,
+    };
+
+    use super::*;
+
+    struct TestImporter {}
+
+    impl ModuleImporter for TestImporter {
+        fn import(&self, path: &ModulePath) -> Option<String> {
+            match path {
+                ModulePath(p) if p == &["foo", "bar"] => Some("use foo::baz".into()),
+                ModulePath(p) if p == &["foo", "baz"] => Some("let a = 1".into()),
+                _ => None,
+            }
+        }
+    }
+
+    #[test]
+    fn resolver_basic_import() {
+        let program = "
+        use foo::bar
+        a
+        ";
+
+        let importer = TestImporter {};
+
+        let resolver = Resolver::new(&importer);
+        let program_inlined = resolver.resolve(&program).unwrap();
+
+        assert_eq!(
+            &program_inlined,
+            &[
+                Statement::DeclareVariable(
+                    "a".into(),
+                    Expression::Scalar(Number::from_f64(1.0)),
+                    None
+                ),
+                Statement::Expression(Expression::Identifier("a".into()))
+            ]
+        );
+    }
+}

+ 4 - 0
numbat/src/tokenizer.rs

@@ -49,6 +49,7 @@ pub enum TokenKind {
     Arrow,
     Equal,
     Colon,
+    ColonColon,
     PostfixApply,
     UnicodeExponent,
     At,
@@ -59,6 +60,7 @@ pub enum TokenKind {
     Fn,
     Dimension,
     Unit,
+    Use,
 
     Long,
     Short,
@@ -189,6 +191,7 @@ impl Tokenizer {
             m.insert("fn", TokenKind::Fn);
             m.insert("dimension", TokenKind::Dimension);
             m.insert("unit", TokenKind::Unit);
+            m.insert("use", TokenKind::Use);
             m.insert("long", TokenKind::Long);
             m.insert("short", TokenKind::Short);
             m.insert("both", TokenKind::Both);
@@ -295,6 +298,7 @@ impl Tokenizer {
             '^' => TokenKind::Power,
             ',' => TokenKind::Comma,
             '=' => TokenKind::Equal,
+            ':' if self.match_char(':') => TokenKind::ColonColon,
             ':' => TokenKind::Colon,
             '@' => TokenKind::At,
             '→' | '➞' => TokenKind::Arrow,

+ 3 - 0
numbat/src/typechecker.rs

@@ -545,6 +545,9 @@ impl TypeChecker {
 
                 typed_ast::Statement::ProcedureCall(kind, checked_args)
             }
+            ast::Statement::ModuleImport(_) => {
+                unreachable!("Modules should have been inlined by now")
+            }
         })
     }