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,