commit 385e056052736bf9d6e6f46fa211f561ab6ae4ca
parent 23c916f2dce148c3ce8e307e93cf7b3debf3dfd5
Author: Demonstrandum <samuel@knutsen.co>
Date:   Wed, 17 Jul 2024 18:03:49 +0100
Added `-I` include directories + `(%namespace ...)` macro.
* You can now provide `-I/my/dir` to the command-line utility to specify
  the potential search-directories that `(%include ...)` should try before
  giving up.
* The `(%namespace NAME ...)` will evaluate the body `...` and replace
  any definitions (variables defined in the scope) to be prefixed with
  `NAME` and the default separator `/`. So,
  ```
  (%namespace ns a b (%define x d) c %x e) f %ns/x g
  ```
  will evaluate to `a b c d e f d g`, where the definition
  `%x` is perfectly accessible within the namespace, but only
  accessible as `%ns/x` outside of the namespace.
  You can provide a separator, so for example you can write
  ```
  (%namespace ns :separator - (%define x d))
  ```
  instead, where you'd access `%x` as `%ns-x` instead.
  The separator can even be the empty string (no separator).
Diffstat:
8 files changed, 151 insertions(+), 69 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -168,7 +168,7 @@ dependencies = [
 
 [[package]]
 name = "seam"
-version = "0.2.4"
+version = "0.2.5"
 dependencies = [
  "chrono",
  "colored",
diff --git a/Cargo.toml b/Cargo.toml
@@ -4,7 +4,7 @@ description = "Symbolic Expressions As Markup."
 keywords = ["markup", "lisp", "macro", "symbolic-expression", "sexp"]
 license-file = "LICENSE"
 homepage = "https://git.knutsen.co/seam"
-version = "0.2.4"
+version = "0.2.5"
 authors = ["Demonstrandum <samuel@knutsen.co>"]
 edition = "2021"
 
diff --git a/README.md b/README.md
@@ -91,6 +91,12 @@ seam --sexp <<< '(hello (%define subject world) %subject)'
 ```
 
 ## Checklist
+ - [ ] `(%define x %body)` evaluates `%body` eagerly (at definition),
+       while `(%define (y) %body)` only evaluates `%body` per call-site `(%y)`.
+ - [x] Namespace macro `(%namespace ns (%include "file.sex"))` will prefix all definitions in its body with `ns/`, e.g. `%ns/defn`.
+       Allows for a customizable separator, e.g. `(%namespace ns :separator "-" ...)` will allow for writing `%ns-defn`.
+       Otherwise, the macro leaves the content produced by the body completely unchanged.
+ - [x] Command line `-I` include directory.
  - [ ] First argument (of body) in a macro invocation should have its whitespace stripped.
  - [x] `(%os/env ENV_VAR)` environment variable macro.
  - [ ] `(%to-string ...)`, `(%join ...)`, `(%map ...)`, `(%filter ...)` macros.
diff --git a/src/bin.rs b/src/bin.rs
@@ -1,6 +1,7 @@
 use seam;
 use seam::assemble::MarkupFormatter;
 
+use std::collections::BTreeSet;
 use std::{io, env};
 use std::path::PathBuf;
 use std::error::Error;
@@ -20,8 +21,10 @@ fn main() -> Result<(), Box<dyn Error>> {
     let mut args = env::args();
     let _ = args.next();  // Discard.
 
+    // Command line flags and options.
     let mut files = Vec::new();
     let mut target = String::from("");
+    let mut include_dirs: BTreeSet<PathBuf> = Default::default();
     let mut from_stdin = false;
     let mut is_doc = true;
 
@@ -38,14 +41,19 @@ fn main() -> Result<(), Box<dyn Error>> {
                        format!("Unknown argument: `--{}'.", opt))
                 }
             } else if let Some(opt) = arg.split("-").nth(1) {
-                match opt {
-                    "v" => {
+                match opt.as_bytes() {
+                    [b'v'] => {
                         let (major, minor, tiny) = seam::VERSION;
                         eprintln!("{}", format!("SEAM v{}.{}.{}",
                             major, minor, tiny).bold());
                         std::process::exit(0);
                     },
-                    "" => {
+                    [b'I', ..] => {
+                        let dir = &arg.as_str()[2..];
+                        let dir = PathBuf::from(dir);
+                        include_dirs.insert(dir);
+                    },
+                    [] => {
                         from_stdin = true;
                     },
                     _ => argument_fatal(
@@ -73,12 +81,14 @@ fn main() -> Result<(), Box<dyn Error>> {
 
     if from_stdin {
         let mut stdin = io::stdin();
-        let builder = seam::tree_builder_stream(&mut stdin)?;
+        let mut builder = seam::tree_builder_stream(&mut stdin)?;
+        builder.add_includes(include_dirs.iter());
         generate_and_print(&builder, &target, is_doc);
     }
 
     for file in files {
-        let builder = seam::tree_builder_file(&file)?;
+        let mut builder = seam::tree_builder_file(&file)?;
+        builder.add_includes(include_dirs.iter());
         generate_and_print(&builder, &target, is_doc);
     }
 
diff --git a/src/lib.rs b/src/lib.rs
@@ -9,7 +9,7 @@ use parse::{expander, parser, lexer};
 
 use std::{fs, io, path::Path};
 
-pub const VERSION: (u8, u8, u8) = (0, 2, 4);
+pub const VERSION: (u8, u8, u8) = (0, 2, 5);
 
 pub fn tree_builder<'a, P: AsRef<Path>>(source_path: Option<P>, string: String)
     -> expander::Expander<'a> {
diff --git a/src/parse/expander.rs b/src/parse/expander.rs
@@ -1,16 +1,18 @@
 use super::parser::{Node, ParseNode, ParseTree, Parser};
 use super::tokens::Site;
 
+use std::fmt::Display;
 use std::{
     fmt,
     cell::RefCell,
-    path::{
-        Path,
-        PathBuf
-    },
+    path::PathBuf,
     ffi::OsString,
     error::Error,
     rc::Rc,
+    collections::{
+        HashMap,
+        BTreeSet,
+    },
 };
 
 use colored::*;
@@ -47,8 +49,6 @@ impl<'a> fmt::Display for ExpansionError<'a> {
 /// Implements std::error::Error for macro expansion error.
 impl<'a> Error for ExpansionError<'a> { }
 
-use std::collections::HashMap;
-
 /// A macro consists of:
 /// - its name;
 /// - its argument list (if any);
@@ -78,6 +78,7 @@ pub type Scope<'a> = RefCell<HashMap<String, Rc<Macro<'a>>>>; // Can you believe
 #[derive(Debug, Clone)]
 pub struct Expander<'a> {
     parser: Parser,
+    includes: BTreeSet<PathBuf>,
     subparsers: RefCell<Vec<Parser>>,
     subcontexts: RefCell<Vec<Self>>,
     invocations: RefCell<Vec<ParseNode<'a>>>,
@@ -88,6 +89,7 @@ impl<'a> Expander<'a> {
     pub fn new(parser: Parser) -> Self {
         Self {
             parser,
+            includes: BTreeSet::from([PathBuf::from(".")]),
             subparsers: RefCell::new(Vec::new()),
             subcontexts: RefCell::new(Vec::new()),
             invocations: RefCell::new(Vec::new()),
@@ -100,23 +102,31 @@ impl<'a> Expander<'a> {
         self.parser.get_source()
     }
 
+    pub fn add_includes<T: Iterator>(&mut self, dirs: T)
+        where T::Item: Into<PathBuf>
+    {
+        for dir in dirs {
+            self.includes.insert(dir.into());
+        }
+    }
+
     /// Add a subparser owned by the expander context.
-    fn register_parser(&'a self, parser: Parser) -> &'a Parser {
+    fn register_parser(&self, parser: Parser) -> &'a Parser {
         {
             let mut parsers = self.subparsers.borrow_mut();
             parsers.push(parser);
         }
-        self.latest_parser()
+        self.latest_parser().unwrap()
     }
 
     /// Get the latest subparser added.
-    fn latest_parser(&'a self) -> &'a Parser {
+    fn latest_parser(&self) -> Option<&'a Parser> {
         let p = self.subparsers.as_ptr();
-        unsafe { (*p).last().unwrap_or(&self.parser) }
+        unsafe { (*p).last() }
     }
 
     /// Create and register a subcontext built from the current context.
-    fn create_subcontext(&'a self) -> &'a Self {
+    fn create_subcontext(&self) -> &mut Self {
         {
             let copy = self.clone();
             let mut contexts = self.subcontexts.borrow_mut();
@@ -126,12 +136,12 @@ impl<'a> Expander<'a> {
     }
 
     /// Get the latest subparser added.
-    fn latest_context(&'a self) -> Option<&'a Self> {
+    fn latest_context(&self) -> Option<&mut Self> {
         let contexts = self.subcontexts.as_ptr();
-        unsafe { (*contexts).last() }
+        unsafe { (*contexts).last_mut() }
     }
 
-    fn register_invocation(&'a self, node: ParseNode<'a>) -> &'a ParseNode<'a> {
+    fn register_invocation(&self, node: ParseNode<'a>) -> &ParseNode<'a> {
         let invocations = self.invocations.as_ptr();
         unsafe {
             (*invocations).push(node);
@@ -140,25 +150,25 @@ impl<'a> Expander<'a> {
     }
 
     /// Update variable (macro) for this scope.
-    fn insert_variable(&'a self, name: String, var: Rc<Macro<'a>>) {
+    fn insert_variable(&self, name: String, var: Rc<Macro<'a>>) {
         let mut defs = self.definitions.borrow_mut();
         defs.insert(name, var);
     }
 
     /// Check if macro exists in this scope.
-    fn has_variable(&'a self, name: &str) -> bool {
+    fn has_variable(&self, name: &str) -> bool {
         let defs = self.definitions.borrow();
         defs.contains_key(name)
     }
 
-    fn get_variable(&'a self, name: &str) -> Option<Rc<Macro<'a>>> {
+    fn get_variable(&self, name: &str) -> Option<Rc<Macro<'a>>> {
         self.definitions.borrow().get(name).map(|m| m.clone())
     }
 
     /// Define a macro with `(%define a b)` --- `a` is a symbol or a list `(c ...)` where `c` is a symbol.
     /// macro definitions will eliminate any preceding whitespace, so make sure trailing whitespace provides
     /// the whitespace you need.
-    fn expand_define_macro(&'a self, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+    fn expand_define_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         let [head, nodes@..] = &*params else {
             return Err(ExpansionError(
@@ -220,7 +230,7 @@ impl<'a> Expander<'a> {
     /// `(%ifdef symbol a b)` --- `b` is optional, however, if not provided *and*
     /// the symbol is not defined, it will erase the whole expression, and whitespace will not
     /// be preseved before it. If that's a concern, provide `b` as the empty string `""`.
-    fn expand_ifdef_macro(&'a self, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+    fn expand_ifdef_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         if params.len() < 2 || params.len() > 3 {
             return Err(ExpansionError(format!("`ifdef` takes one (1) \
@@ -253,7 +263,7 @@ impl<'a> Expander<'a> {
         Ok(expanded)
     }
 
-    fn expand_include_macro(&'a self, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+    fn expand_include_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         let params: Box<[ParseNode<'a>]> = self.expand_nodes(params)?;
         let [path_node] = &*params else {
@@ -264,38 +274,44 @@ impl<'a> Expander<'a> {
                 node.site().to_owned()));
         };
 
-        let Some(Node { value: path, .. }) = path_node.atomic()  else {
+        let Some(Node { value: path, site, .. }) = path_node.atomic()  else {
             return Err(ExpansionError(
                 "Bad argument to `%include' macro.\n\
                     Expected a path, but did not get any value
                     that could be interpreted as a path.".to_string(),
-                node.site().to_owned()))
+                path_node.site().to_owned()))
         };
 
         // Open file, and parse contents!
-        let path = Path::new(&path);
-        let parser = match super::parser_for_file(&path) {
-            Ok(parser) => parser,
-            Err(error) => {
-                let err = ExpansionError(
-                    format!("{}", error), node.site().to_owned());
-                // Try with `.sex` extensions appended.
-                let mut with_ext = PathBuf::from(path);
-                let filename = path.file_name().ok_or(err.clone())?;
-                with_ext.pop();
-
-                let mut new_filename = OsString::new();
-                new_filename.push(filename);
-                new_filename.push(".sex");
-
-                with_ext.push(new_filename);
-                match super::parser_for_file(&with_ext) {
-                    Ok(parser) => parser,
-                    Err(_)   => return Err(err)
-                }
-            }
-        };
-        let parser = self.register_parser(parser);
+        let include_error = |error: Box<dyn Display>| ExpansionError(
+            format!("{}", error), site.to_owned());
+        let mut parser: Result<Parser, ExpansionError> = Err(
+            include_error(Box::new("No path tested.")));
+        // Try all include directories until one is succesful.
+        for include_dir in &self.includes {
+            let path = include_dir.join(path);
+            parser = super::parser_for_file(&path)
+                .or_else(|err| {
+                    let err = Box::new(err);
+                    // Try with `.sex` extensions appended.
+                    let mut with_ext = PathBuf::from(&path);
+                    let filename = path.file_name()
+                        .ok_or(include_error(err))?;
+                    with_ext.pop();  // Remove old filename.
+                    // Build new filename with `.sex` appended.
+                    let mut new_filename = OsString::new();
+                    new_filename.push(filename);
+                    new_filename.push(".sex");
+                    with_ext.push(new_filename); // Replace with new filename.
+                    match super::parser_for_file(&with_ext) {
+                        Ok(parser) => Ok(parser),
+                        Err(err)   => Err(include_error(Box::new(err)))
+                    }
+                });
+            if parser.is_ok() { break; }
+        }
+        // Register the parser for the found file.
+        let parser = self.register_parser(parser?);
         let tree = match parser.parse() {
             Ok(tree) => tree,
             Err(error) => return Err(ExpansionError(
@@ -316,7 +332,7 @@ impl<'a> Expander<'a> {
         Ok(expanded_tree.into_boxed_slice())
     }
 
-    fn expand_date_macro(&'a self, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+    fn expand_date_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         let params = self.expand_nodes(params)?;
         let [date_format] = &*params else {
@@ -343,7 +359,7 @@ impl<'a> Expander<'a> {
 
     /// `(%log ...)` logs to `STDERR` when called and leaves *no* node behind.
     /// This means whitespace preceeding `(%log ...)` will be removed!
-    fn expand_log_macro(&'a self, node: &'a ParseNode<'a>, params: ParseTree<'a>)
+    fn expand_log_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         let mut words = Vec::with_capacity(params.len());
         for param in self.expand_nodes(params)? {
@@ -440,7 +456,7 @@ impl<'a> Expander<'a> {
         }
     }
 
-    fn expand_macro(&'a self, name: &str, node: &'a ParseNode<'a>, params: ParseTree<'a>)
+    fn expand_macro(&self, name: &str, node: &ParseNode<'a>, params: ParseTree<'a>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         // Eagerly evaluate parameters passed to macro invocation.
         let params = self.expand_nodes(params)?;
@@ -477,26 +493,67 @@ impl<'a> Expander<'a> {
         Ok(expanded.into_boxed_slice())
     }
 
-    fn expand_invocation(&'a self,
+    fn expand_namespace_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>)
+    -> Result<ParseTree<'a>, ExpansionError<'a>> {
+        // Start evaluating all the arguments to the macro in a separate context.
+        let context = self.clone();
+        let params =  context.expand_nodes(params)?;
+        let mut args = params.iter().peekable();
+        let Some(namespace) = args.next().and_then(ParseNode::atomic) else {
+            return Err(ExpansionError::new("Expected a namespace name.", node.site()));
+        };
+        // Parse options to macro.
+        let mut seperator = "/";  // Default namespace seperator is `/`.
+        while let Some(ParseNode::Attribute { keyword, node, site, .. }) = args.peek() {
+            let _ = args.next();
+            match keyword.as_str() {
+                "separator" => match node.atomic() {
+                    Some(Node { value, .. }) => seperator = &value,
+                    None => return Err(ExpansionError(
+                        format!("`%namespace' separator must be a symbol, got a {}.", node.node_type()),
+                        node.owned_site())),
+                },
+                opt => return Err(ExpansionError(
+                    format!("Unknown option `:{}' to `%namespace' macro.", opt),
+                    site.clone())),
+            }
+        }
+        // Find all the definitions made within the context of the
+        // `%namespace` macro and include the defintion prefixed by
+        // the namespace in the *current* scope.
+        {
+            let mut self_defs = self.definitions.borrow_mut();
+            let defs = context.definitions.borrow();
+            for (key, value) in defs.iter() {
+                let new_key = format!("{}{}{}", namespace.value, seperator, key);
+                self_defs.insert(new_key, value.clone());
+            }
+        }
+        // Return remaining body of the macro.
+        Ok(args.cloned().collect())
+    }
+
+    fn expand_invocation(&self,
                          name: &str, //< Name of macro (e.g. %define).
-                         node: &'a ParseNode<'a>, //< Node for `%'-macro invocation.
+                         node: &ParseNode<'a>, //< Node for `%'-macro invocation.
                          params: Box<[ParseNode<'a>]> //< Passed in arguments.
     ) -> Result<ParseTree<'a>, ExpansionError<'a>> {
         // Some macros are lazy (e.g. `ifdef`), so each macro has to
         //   expand the macros in its arguments individually.
         match name {
-            "define"  => self.expand_define_macro(node, params),
-            "ifdef"   => self.expand_ifdef_macro(node, params),
-            "include" => self.expand_include_macro(node, params),
-            "date"    => self.expand_date_macro(node, params),
-            "log"     => self.expand_log_macro(node, params),
-            "format"  => self.expand_format_macro(node, params),
-            "os/env"  => self.expand_os_env_macro(node, params),
-            _         => self.expand_macro(name, node, params),
+            "define"    => self.expand_define_macro(node, params),
+            "ifdef"     => self.expand_ifdef_macro(node, params),
+            "include"   => self.expand_include_macro(node, params),
+            "namespace" => self.expand_namespace_macro(node, params),
+            "date"      => self.expand_date_macro(node, params),
+            "log"       => self.expand_log_macro(node, params),
+            "format"    => self.expand_format_macro(node, params),
+            "os/env"    => self.expand_os_env_macro(node, params),
+            _           => self.expand_macro(name, node, params),
         }
     }
 
-    pub fn expand_node(&'a self, node: ParseNode<'a>)
+    pub fn expand_node(&self, node: ParseNode<'a>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         match node {
             ParseNode::Symbol(ref sym) => {
@@ -569,7 +626,7 @@ impl<'a> Expander<'a> {
         }
     }
 
-    pub fn expand_nodes(&'a self, tree: Box<[ParseNode<'a>]>)
+    pub fn expand_nodes(&self, tree: Box<[ParseNode<'a>]>)
     -> Result<ParseTree<'a>, ExpansionError<'a>> {
         let mut expanded = Vec::with_capacity(tree.len());
         for branch in tree {
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
@@ -58,6 +58,15 @@ impl<'a> ParseNode<'a> {
         }
     }
 
+    pub fn into_atomic(self) -> Option<Node<'a>> {
+        match self {
+            Self::Symbol(node)
+            | Self::Number(node)
+            | Self::String(node) => Some(node),
+            _ => None
+        }
+    }
+
     pub fn site(&self) -> &Site<'a> {
         match self {
             Self::Symbol(ref node)
diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs
@@ -4,7 +4,7 @@ use unicode_width::UnicodeWidthStr;
 #[derive(Debug, Clone)]
 pub struct Site<'a> {
     pub source: &'a str,
-    pub source_code: &'a str, // TODO: propagate!
+    pub source_code: &'a str,
     pub line: usize,
     pub bytes_from_start: usize,
     pub bytes_from_start_of_line: usize,