seam

Symbolic-Expressions As Markup.
git clone git://git.knutsen.co/seam
Log | Files | Refs | README | LICENSE

commit ab2de7c73651607ed1993e1ff8c8dc1dae5e477b
parent 475e2488777ac9605059b77b0cb7d47001eec6aa
Author: Demonstrandum <samuel@knutsen.co>
Date:   Sun,  8 Dec 2024 17:27:45 +0000

Added `%match` macro to do pattern matching.

Diffstat:
MCargo.lock | 2+-
MREADME.md | 6+++---
Mcrates/seam/Cargo.toml | 2+-
Mcrates/seam/src/parse/expander.rs | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 113 insertions(+), 15 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -297,7 +297,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "seam" -version = "0.4.0" +version = "0.4.1" dependencies = [ "chrono", "colored", diff --git a/README.md b/README.md @@ -92,11 +92,11 @@ seam --sexp <<< '(hello (%define subject world) %subject)' ## Checklist - [ ] User `(%error msg)` macro for aborting compilation. - - [ ] Pattern-matching `(%match expr (pat1 ...) (pat2 ...) default)` macro. + - [x] Pattern-matching `(%match expr (pat1 ...) (pat2 ...))` macro. Pattern matching is already implemented for `%define` internally. - - [ ] The trailing keyword-matching operator. `&&rest` matches excess keyword. + - [x] The trailing keyword-matching operator. `&&rest` matches excess keyword. Extracting a value from a map `(:a 1 :b 2 :c 3)` is done with: - `(%match h (:b () &&_) %b)`. + `(%match %h ((:b default &&_) %b))`. - [x] `%get` macro: `(%get b (:a 1 :b 2))` becomes `2`; `(%get 0 (a b c))` becomes `a`. - [x] `(%yaml "...")`, `(%toml "...")` and `(%json "...")` converts whichever config-lang definition into a seam `%define`-definition. diff --git a/crates/seam/Cargo.toml b/crates/seam/Cargo.toml @@ -5,7 +5,7 @@ keywords = ["markup", "lisp", "macro", "symbolic-expression", "sexp"] license-file = "../../LICENSE" readme = "../../README.md" homepage = "https://git.knutsen.co/seam" -version = "0.4.0" +version = "0.4.1" authors = ["Demonstrandum <samuel@knutsen.co>"] edition = "2021" diff --git a/crates/seam/src/parse/expander.rs b/crates/seam/src/parse/expander.rs @@ -1,6 +1,7 @@ use super::parser::{Node, ParseNode, ParseTree, Parser}; use super::tokens::Site; +use std::f32::consts::E; use std::{ fmt, cell::RefCell, @@ -251,10 +252,11 @@ impl<'a> Expander<'a> { fn bind_list(&self, assigned: &ParseNode<'a>, nodes0: &ParseTree<'a>, nodes1: &ParseTree<'a>) -> Result<(), ExpansionError<'a>> { let mut rest_node = None; + let mut rest_kw_node = None; let mut rhs_index: usize = 0; let mut expected: usize = 0; - let mut rhs_named = HashMap::new(); - let mut lhs_named = HashMap::new(); + let mut rhs_named = HashMap::new(); // Contains keyword -> attribute node. + let mut lhs_named = HashMap::new(); // Contains keyword -> attr. value node (defaults). // We loop this way (not a for loop) so we can control // when exactly we advance to the next LHS node, potentially // doing multiple iterations on the same node. @@ -270,6 +272,24 @@ impl<'a> Expander<'a> { maybe_node0 = nodes0_iter.next(); continue; } + // Check for &&-rest keyword matching syntax. + let found_kw_rest = node0.symbol().map(|name| name.value.starts_with("&&")).unwrap_or(false); + if found_kw_rest { + // If another &&-rest node has been found, report an error. + if rest_kw_node.is_some() { + return Err(ExpansionError::new( + "Found multiple nodes matching named argument &&-rest syntax.", + node0.site(), + )); + } + // Otherwise, make note of the node it corresponds to. + rest_kw_node = Some(node0); + // Note that we don't increment the `rhs_index`, + // since a &&rest node does not match the corresponding item in the RHS. + maybe_node0 = nodes0_iter.next(); + continue; + } + // Check for &-rest regular argument matching syntax. let found_rest = node0.symbol().map(|name| name.value.starts_with('&')).unwrap_or(false); if found_rest { // If another &-rest node has been found, report an error. @@ -294,11 +314,11 @@ impl<'a> Expander<'a> { )); } let node1 = &nodes1[rhs_index]; - if let ParseNode::Attribute { keyword, node, .. } = node1 { + if let ParseNode::Attribute { keyword, .. } = node1 { // This is a named argument given in the RSH, so it does not correspond to // the specific non-named argument in the LHS, so we keep looking until we // get to it, and remember all the named arguments we find along the way. - rhs_named.insert(keyword.clone(), node); + rhs_named.insert(keyword.clone(), node1); rhs_index += 1; // Continue without advancing to the next LHS `node0`. continue; @@ -314,7 +334,7 @@ impl<'a> Expander<'a> { let node1 = &nodes1[rhs_index]; if let ParseNode::Attribute { keyword, node, .. } = node1 { // There might be remaining named argument further down the RHS list. - rhs_named.insert(keyword.clone(), node); + rhs_named.insert(keyword.clone(), node1); } else { rest.push(node1.clone()); } @@ -346,7 +366,7 @@ impl<'a> Expander<'a> { // Remove memory of assigned node from RHS. let value = match rhs_named.remove(*keyword) { // Found the named argument in the RHS, so don't use the default. - Some(value) => value, + Some(attr) => attr.attribute().unwrap().1, // No named corresponding argument in the RHS means we have to use its default. None => default, }; @@ -360,6 +380,24 @@ impl<'a> Expander<'a> { }), ); } + // Capture remaining named arguments under the &&-macro. + if let Some(rest_kw_node) = rest_kw_node { + let rest_kw_symbol = rest_kw_node.symbol().unwrap(); + let rest_kw_name = &rest_kw_symbol.value[2..]; + // Collect the named arguments. + let attrs: Vec<String> = rhs_named.keys().cloned().collect(); + let mut nodes = Vec::with_capacity(attrs.len()); + for attr in attrs { + let named = rhs_named.remove(&attr).unwrap(); + nodes.push(named.clone()); + } + // Insert the &&-variable. + self.insert_variable(rest_kw_name.to_string(), Rc::new(Macro { + name: rest_kw_name.to_string(), + params: Box::new([]), + body: nodes.into_boxed_slice(), + })); + } // Any remaining RHS named nodes not covered by the LHS, are excess/errors. if !rhs_named.is_empty() { // Go through RHS named nodes and list all the excess/invalid names. @@ -403,6 +441,53 @@ impl<'a> Expander<'a> { Ok(()) } + /// `%(match expr (pattrern1 body1) (pattern2 body2) ...)` + fn expand_match_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>) + -> Result<ParseTree<'a>, ExpansionError<'a>> { + let (expr, patterns) = match &*params { + [expr, patterns@..] => (expr, patterns), + _ => return Err(ExpansionError::new( + "Match syntax is: `(%match expr [...(pattern value)])`.", + node.site(), + )), + }; + let [expr,] = &*self.expand_node(expr.clone())? else { + return Err(ExpansionError::new( + "Value to match against must be single value.", + expr.site(), + )); + }; + for pattern in patterns { + let Some(pattern_list) = pattern.list() else { + return Err(ExpansionError::new( + "Pattern in `%match` must be a list `(pattern ...value)`.", + pattern.site(), + )); + }; + let [pattern, value@..] = &**pattern_list else { + return Err(ExpansionError::new( + "Empty pattern not allowed in `%match`.", + pattern.site(), + )); + }; + let [pattern,] = &*self.expand_node(pattern.clone())? else { + return Err(ExpansionError::new( + "Pattern must evaluate to a single node.", + pattern.site(), + )); + }; + // Now attempt to `bind` against pattern, successful binds means + // we evaluate the RHS of the list and return that. + let subcontext = self.clone(); // Subscope. + if let Ok(()) = subcontext.bind(pattern, expr) { + return subcontext.expand_nodes(value.into()); + } + } + + // No match means no nodes are produced. + Ok(Box::new([])) + } + /// 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. @@ -1239,9 +1324,11 @@ impl<'a> Expander<'a> { fn create_macro(&self, name: String, arglist: Vec<ParseNode<'a>>, body: ParseTree<'a>) -> Result<Rc<Macro<'a>>, ExpansionError<'a>> { - // Check excess &-macros are not present. + // Check excess &-macros and &&-macros are not present. let rest_params: Vec<&ParseNode> = arglist.iter() - .filter(|node| node.symbol().map(|name| name.value.starts_with('&')).unwrap_or(false)) + .filter(|node| node.symbol().map( + |name| name.value.starts_with('&') && !name.value.starts_with("&&")) + .unwrap_or(false)) .collect(); match rest_params.as_slice() { [_, excess, ..] => return Err(ExpansionError::new( @@ -1250,6 +1337,16 @@ impl<'a> Expander<'a> { )), _ => {} }; + let kw_rest_params: Vec<&ParseNode> = arglist.iter() + .filter(|node| node.symbol().map(|name| name.value.starts_with("&&")).unwrap_or(false)) + .collect(); + match kw_rest_params.as_slice() { + [_, excess, ..] => return Err(ExpansionError::new( + "Excess `&&`-variadic named argument capture variables.", + excess.site() + )), + _ => {} + }; // Create and insert macro. let mac = Rc::new(Macro { @@ -1297,6 +1394,7 @@ impl<'a> Expander<'a> { // expand the macros in its arguments individually. match name { "define" => self.expand_define_macro(node, params), + "match" => self.expand_match_macro(node, params), "ifdef" => self.expand_ifdef_macro(node, params), "do" => self.expand_do_macro(node, params), "get" => self.expand_get_macro(node, params), @@ -1722,12 +1820,12 @@ fn expand_toml<'a>(context: &Expander<'a>, text: &str, sep: &str, site: &Site<'a site: self.site, leading_whitespace: whitespace(leading), }), - toml::Value::Datetime(date) => ParseNode::Number(Node { + toml::Value::Datetime(date) => ParseNode::Symbol(Node { value: format!("{}", date), site: self.site, leading_whitespace: whitespace(leading), }), - toml::Value::Boolean(boolean) => ParseNode::Number(Node { + toml::Value::Boolean(boolean) => ParseNode::Symbol(Node { value: format!("{}", boolean), site: self.site, leading_whitespace: whitespace(leading),