head| meta| @| charset| utf-8 link| @| href| https://florida.moe/assets/treeside-simple.css rel| stylesheet type| text/css nh1| nitrate — Combinator Based Pattern Matcher {href `https://codeberg.org/phm/nitrate` Git repository (latest)} {href `https://codeberg.org/phm/nitrate/src/tag/1.0` Git repository (1.0)} {href `https://florida.moe/nitrate/nitrate-1.0.tgz` Release tarball (1.0)} Nitrate is a combinator (procedure) based backtracking pattern matcher. Being procedure based, as opposed to syntax based, the matcher is extensible with any predicate or record type and its basic components are composable and can represent recursive structure. Designed for {R7RS}-small systems without fancy macro systems or optimizers. This could also be called monadic pattern matching or a parser-combinator library for Scheme values. Nitrate has been tested to work on CHICKEN 5 and Chibi Scheme. the-table-of-contents| nh2| Why a Procedure-Based Pattern Matcher? Scheme has no standard pattern matcher. A commonly used one is the “Wright” pattern matcher, with a notable `syntax-rules` implementation using CPS macros by Alex Shinn. Such patterns matchers are not extensible. Some proposals for high-end, extensible pattern matchers include {SRFI 257} and {SRFI 262}. These matchers are all at the syntax level and are very difficult to implement and extend as a result. A static-typed language like Standard ML or {href `https://www.haskell.org/` Haskell} requires a matcher as a syntactic construct because it has no way to discriminate against sum types. But Scheme is a latently typed language, so it does not have this issue. Higher-order procedures should be able to do the same thing that pattern matching syntax can, with the added benefit of extensibility. This procedure-based pattern matcher is very composable. For instance, the pattern {idref or~} could be defined as: code| pre| (define {idref or~} (case-lambda (() {idref _~}) ((x~) x~) ((x~ . spats) ({idref if~} x~ {idref _~} (apply {idref or~} spats))))) This code almost looks like how one would write `or` in a call-by-name version of Scheme. Since procedures are composed before matching, the code that makes up the patterns passed to {idref or~} are not run until then. The composition is done over a finite list (the argument list), hence the recursion is done at a meta-level to the matching. But with this system, one can also write recursive patterns at the matching level. Consider the following possible implementation of {idref member~}: code| pre| ({idref define-recpat} (member~ pat~) ({idref or~} ({idref cons~} pat~ {idref _~}) ({idref cons~} {idref _~} (member~ pat~)))) The macro {idref define-recpat} uses an eta-expansion trick to allow for a composition of matching procedures to be recursive. A procedure-based pattern matcher would have a uniform syntax for matching any structured data. This may make the basic case of lists and vectors less ergonomic than a specalized solution, but this could encourage people to use abstract patterns instead of concrete ones. One thing that a procedure-based pattern matcher can't do that a syntactic pattern matcher can is to bind values to identifiers, because identifiers are a syntactic construct. This has its upsides, though: a binding can appear zero or more times, and the client code can extract values as desired. nh2| Design of the Matcher The matcher is a depth-first, left-to-right traversal. Many {R7RS}-small systems are not going to take much opportunity to optimize anyways by re-arranging the evaulation order, so by fixing an evaluation order the matcher can express things such as cuts. The matcher threads an immutable map through the match. Its immutability makes it easy to backtrack. The map is returned at the end of the match. The matcher procedures either return the map, or `#f`. The matcher backtracks by maintaining a stack of continuations captured by `call/cc`. This means that failures jump, they do not flow up. This matcher only uses escape continuations by default, so even for implementations with an inefficient `call/cc` this implementation can still work with something like `call/ec` or `call/1cc`. The default matcher is `call/cc` safe. That means that a matcher using only the procedures here can be entered and exited multiple times. This could be used, for instance, to match one branch, do some processing, and cut back to try another match if something goes wrong in the client code. nh3| Future Directions One idea is to use identifiers ({R6RS}) instead of symbols. This would make the patterns easier to read and help with possible scope and hygiene issues. A more radical change would be to use {SRFI 247} style syntactic monads. Another possibility is to do a version of {SRFI 262} lowered to the procedure level. SRFI 262 is designed to be compilable efficiently, which suggests a route for optimizing compilers: given a pattern (i.e. in a procedure), lift it such that it only needs to be evaluated once, evaluate derived matching procedures into matching primitives (maybe record types), optimize the primitives, and then output a matching procedure with compiler hints to aggresively inline. nh2| Examples An example of a toy expander for the R4RS special forms is bundled in the repository in the `examples` folder. Match a list with specified atoms: code| pre| (define (one-two-three-four lst) ({idref match} lst dict (({idref list~} 1 2 3 4) #t) ({idref _~} #f))) (one-two-three-four '(1 2 3 4)) ; => #t (one-two-three-four '#(1 2 3 4)) ; => #f Match a list, mapping values to symbols: code| pre| (define (simple lst) ({idref match} lst dict (({idref list~} 1 ({idref b~} 'second) 3) (car (dict-ref (dto) dict 'second))) ({idref _~} #f))) (simple '(1 100 3)) ; => 100 (simple '(1 100 5~~ ; => #f Match as many numbers at the head of a list as possible: code| pre| (define (leading lst) ({idref match} lst dict (({idref not~} ({idref ?~} pair?)) (values '() '())) (({idref list*~} ({idref and~} ({idref ?~} number?) ({idref b~} 'n)) ({idref b~} 'rest)) (values (dict-ref/default (dto) dict 'n '()) (car (dict-ref (dto) dict 'rest)))))) (leading '(1 2 3 4)) ; => (values '(1 2 3 4) '()) (leading '(1 2 3 x y 5)) ;=> (values '(1 2 3) '(x y 5)) (leading '(x 1 2)) ; => (values '() '(x 1 2)) (leading 'abcd) ; => (values '() '()) Partition a list using a predicate: code| pre| (define (partition predicate? lst) ({idref match} lst dict (({idref list*~} ({idref or~} ({idref and~} ({idref ?~} predicate?) ({idref b~} 'hit)) ({idref b~} 'miss)) '()) (values (dict-ref/default (dto) dict 'hit '()) (dict-ref/default (dto) dict 'miss '()))))) (partition positive? '(1 2 3 -5 -6 7 8 -9)) ; => (values '(1 2 3 7 8) '(-5 -6 -9)) nh2| API nh3| High-Level Matching feature| syntax| {name match} {non-terminal expression} {non-terminal identifier} {gstar ({non-terminal pattern} {non-terminal body})} Each {non-terminal pattern} must evaluate to something that can be passed to {idref mt}. This is generally a matching procedure. Matches {non-terminal expression} to each of the {non-terminal pattern} in turn. If a match results in a success, bind the resulting dictionary to {non-terminal identifier}, with all entries in left-to-right matching order, and tail-evaluate {non-terminal body} with {non-terminal identifier} in scope. feature| syntax| {name match-pr} {non-terminal expression} ({non-terminal identifier{sub 1}} {non-terminal identifier{sub 2}}) {gstar ({non-terminal pattern} {non-terminal body})} Like {idref match}, except instead of binding the dictionary to an identifier: li| the identifier {non-terminal identifer{sub 1}} is bound to a procedure `(r x [d])`. It looks up the symbol `x` in the dictionary, returning the resulting list, or `d` (defaults to the empty list) if not found. li| the identifier {non-terminal identifier{sub 2}} is bound to a procedure `(r1 x [d])`. It looks up the symbol `x` in the dictionary, returning the first matched result, or `d` (defaults to `#f`) if not found. feature| syntax| {name match-rev} {non-terminal expression} {non-terminal identifier} {gstar ({non-terminal pattern} {non-terminal body})} Like {idref match}, except that the dictionary is returned without transforming the order of the bound values. nh3| Dictionaries {index {l {e `dict`}} {ref `dict` argument parameter definition}} All procedure arguments that start with {label `dict` `dict`} or are described as dictionaries must be objects that are {code (dictionary? ({ref dto}) dict)} when passed to the procedure. feature| procedure| name| dto returns| name| dto A parameter object that contains an {SRFI 225} DTO. feature| procedure| name| empty-dict returns| pred| `dict` A parameter object containing a dictionary with no elements. nh3| Backtracking feature| procedure| name| backtrack-stack returns| name| list A parameter object that contains a stack of pairs, the car being any value and the cdr being a captured continuation. The value in the parameter object must have the structure of an assocation list, but it is common for the object to have multiple keys be `eqv?`. feature| procedure| name| fail Invokes the last continuation pushed to {idref backtrack-stack} with the value `#f`. feature| syntax| {name setup-backtrack} {non-terminal marker} {non-terminal expression{sub 1}} {non-terminal expression{sub 2}} Evaluate {non-terminal expression{sub 1}} with a {idref backtrack-stack} that pushes {code (cons marker k)} to {idref backtrack-stack}. If the evaluation of {non-terminal expression{sub 1}} returns `#f`, or if the continuation {var k} is invoked with `#f`, then the expression {non-terminal expression{sub 2}} is evaluated with the same {idref backtrack-stack} as the invocation of {idref setup-backtrack}. nh3| Creating Matching Procedures feature| procedure| name| matcher-procedure args| name| procedure returns| pred| `matcher-procedure?` mp procedure| name| matcher-procedure? args| name| obj returns| name| boolean procedure| name| matcher-procedure-proc args| pred| `matcher-procedure?` mp returns| name| procedure A matcher procedure (mp) is a simple immutable record type that wraps a procedure used to match values. This gives some type safety over using normal procedures. A matcher procedure takes two arguments: a {ref `dict` dictionary} and an arbitrary Scheme value called the “input.” When the match succeeds, it returns a dictionary. If it fails, it invokes {ref fail}: it does not return normally. {index {l {e `tail position`}} {ref `in the tail-position` matcher}} When a matcher is called {label `in the tail-position` {em in the tail-position}}, that means that the continuation of the invocation of that matcher procedure is the continuation of the invoking procedure, like a regular tail call. A series of calls in the tail-position has the same recursion guarantee as a series of tail-calls for normal procedures. For example, consider the following matcher: code| pre| ({idref define-recpat} (member~ pat~) ({idref or~} ({idref cons~} pat~ {idref _~}) ({idref cons~} {idref _~} (member~ pat~)))) The final matcher of {idref or~} is called in the tail-position. Hence this matcher could be used on an arbitrarily large list without causing excessive stack frame allocation. A more explicit version of this is code| pre| ({idref define-matcher} ((member~ pat~) dict input) (let loop ((input input)) (if (not (pair? input)) ({idref fail}) (let ((val ({idref setup-backtrack} ({idref mt} pat~ dict (car input)) #f)) (if val val (loop (cdr input))))))) This is an explicitly tail-recursive loop and hence does not push the stack. This is much more verbose, and the tail-call guarantee of the other procedures is designed to allow for succint recursive matchers. feature| syntax| {name lambda-matcher} ({gopt {non-terminal formals}} {non-terminal identifier{sub 1}} {non-terminal identifier{sub 2}}) {non-terminal body} If {non-terminal formals} is present, create a procedure with those formals that returns a matcher procedure. If it is not present, return a matcher procedure with the two specified formals. feature| syntax| {name define-matcher} ({non-terminal define head} {non-terminal identifier{sub 1}} {non-terminal identifier{sub 2}}) {non-terminal body} Equivalent to: code| pre| (define {non-terminal define head} ({idref lambda-matcher} ({non-terminal identifier{sub 1}} {non-terminal identifier{sub 2}}) {non-terminal body})) feature| syntax| {name define-recpat} {non-terminal define-head} {non-terminal body} Define an eta-expanded composition of matching procedures. This is useful to express recursive pattern matching that would normally cause a call-by-value evaulation order to loop forever. For example, consider the following matcher: code| pre| ({idref define-recpat} (member~ pat~) ({idref or~} ({idref cons~} pat~ {idref _~}) ({idref cons~} {idref _~} (member~ pat~)))) If this was defined using `define`, then `(member~ pat~)` would loop forever as it will call `(member~ pat~)` again. The `define-recpat` version is equivalent to code| pre| (define (member~ pat~) ({idref lambda-matcher} (dict input) ({idref or~} ({idref cons~} pat~ {idref _~}) ({idref cons~} {idref _~} (member~ pat~))))) Hence the evaluation of `(member~ pat~)` stops at the {idref lambda-matcher}. feature| syntax| {name matcher-wrapper} ({non-terminal predicate} {gplus {non-terminal accessor}}) All the arguments to the macro must evaluate to procedures. Create a procedure that in turn creates matching procedures. The procedure takes the same number of arguments as {non-terminal accessor}s, which must be matching procedures. The procedure returns a matching procedure that fails if the passed value does not satisfy {non-terminal predicate}. Otherwise, it matches the first {non-terminal accessor} with the input dictionary, the second {non-terminal accessor} with the returned dictionary, and so on, returning the result of the final {non-terminal accessor}. The final {non-terminal accessor} is called in the tail-position. nh3| Matching {index {l {e `~`}} {ref `tilde` naming convention}} {label `tilde` Identifers ending with `~` are either values that can be passed to {idref `mt`}, or procedures that create values that can be passed to {idref `mt`}.} feature| procedure| name| mt args| pred| `tilde` mat~ pred| `dict` dict name| input returns| name| {ref `dict` dict}/#f Matches {var mat~} against {var input}. li| If {var mat~} is a {idref `matcher-procedure?`}, then tail-call the underlying procedure with {var dict} and {var input} as arguments. li| If the {var mat~} is a normal procedure, raise an error. li| If the {var mat~} is `equal?` to {var input}, return the input dict. li| Otherwise, invoke {ref fail}. nh3| Unconditional Matchers feature| procedure| name| b~ args| name| symbol opt| name| procedure returns| pred| `matcher-procedure?` Creates a matcher that unconditionally matches the passed value. The returned dictionary maps {var symbol} to a list where the head is `(procedure value)`, and {var value} is the passed value. If {var procedure} is not given, the default is the identity function. If the only variable bindings in the matche are made with this procedure, then the resulting dictionary will have the bindings of the match in reverse order. feature| procedure| name| _~ args| pred| `dict` dict name| input returns| pred| `dict` dict A matching procedure that always suceeds and returns the input dictionary. feature| procedure| name| fail~ args| pred| `dict` dict name| input A matching procedure that always fails. nh3| Conditional Matchers feature| procedure| name| if~ args| opt| name| marker pred| `matcher-procedure?` test~ pred| `matcher-procedure?` consequent~ pred| `matcher-procedure?` alternative~ returns| pred| `matcher-procedure?` Try to match {var test~} with a failure continuation pushed to the top of the {idref backtrack-stack} with the {var marker} (defaults to `#f`). If the match of {var test~} succeeds, try to match {var consequent~} in the tail-position with the returned dictionary. Otherwise try to match {var alternative~} in the tail-position with the input dictionary. The use of an explicit marker along with the {idref cut~} matcher allows one to prune the search space of a conditional match. feature| procedure| name| if*~ args| rep| pred| `matcher-procedure?` test~ pred| `matcher-procedure?` consequent~ An analogue of `cond`. Equivalent to: code| pre| ({idref if~} test{sub 1}~ consequent{sub 1}~ ({idref if~} test{sub 2}~ consequent{sub 2}~ ({idref if~} … {idref fail~}))) feature| procedure| name| and~ args| rep| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Create a matching procedure that will try to match the first {var pat~}, and then match the second {var pat~} with the same input and the dictionary of the first {var pat~}, and so on. If one fails, the entire match fails. The final {var pat~} is tail-called. feature| procedure| name| or~ args| rep| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Create a matching procedure that tries each {var pat~} in sequence against the same input and dictionary. If a match fails, the next match is tested. If a match succeeds, return the dictionary from it. If no match succeeds, the match fails. The final {var pat~} is tail-called. feature| procedure| name| not~ args| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Create a matching procedure that will fail if {var pat~} succeeds, and suceed with the input dictionary if {var pat~} fails. feature| procedure| name| opt~ args| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Creating a matching procedure that matches {var pat~} against the input. If the match succeeds, return the value. Otherwise return the input dictionary. The maching procedure always succeeds. feature| procedure| name| as-many~ args| rep| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Creates a matching procedure that attempts to match each {var pat~} against the input. The successful matches are merged from left to right. The maching procedure always succeeds. nh3| Calling Scheme Procedures feature| procedure| name| ?~ args| name| procedure returns| pred| `matcher-procedure?` Creates a matching procedure that will pass its input to {var procedure}. If {var procedure} returns false, the match fails. Otherwise return the input {ref `dict` dictionary} unchanged. feature| procedure| name| inspect~ args| name| predicate returns| pred| `matcher-procedure?` Creates a matching procedure that calls {var predicate} with one argument, the input dictionary. If {var predicate} returns false, the match fails. Otherwise return the input {ref `dict` dictionary} unchanged. feature| procedure| name| xfrm~ args| name| procedure pred| `matcher-procedure?` pat~ returns| pred| `matcher-procedure?` Creates a matching procedure that tail-calls {var pat~} with the output of calling {var procedure} on the input value. nh3| Manipulating the Backtracking Stack feature| procedure| name| cut~ args| name| marker pred| `matcher-procedure?` pat~ returns| pred| `matcher-procedure?` Creates a matching procedure that that will prune the {idref backtrack-stack} up to {var marker} (keeping it on the stack), and then attempting to match {var pat~}. The dynamic extent of the match is otherwise the same. Note that if {var pat~} matches successfully, then any subsequent matches (composed using {idref and~}, for example) will use the backtracking stack as it was before {idref cut~}. The match only affects the dynamic extent of {var pat~}. nh3| Numbers feature| procedure| name| =~ args| name| x returns| pred| `matcher-procedure?` Creates a matching procedure that will succeed only if the input is a number that is numerically equal to {var x}. The procedure will return an empty {ref `dict` dictionary} for a succesful match. feature| procedure| name| =Reps~ args| name| eps name| x returns| pred| `matcher-procedure?` Creates a matching procedure that will succeed only if the input is a number that is less than {var eps} in distance from {var x}, measured by absolute value. The returned dictionary is the input dictionary. feature| procedure| name| signmag~ args| pred| `matcher-procedure` sign~ pred| `matcher-procedure` mag~ returns| pred| `matcher-procedure?` Creates a matching procedure that will succeed if the input is a real number, and the sign (as `-1`, or `1`) matches {var sign~}, and the magnitude (as a non-negative real number) matches {var mag~}. Exact zero and inexact positive zero have a positive sign. Inexact negative zero, if it exists, has a negative sign. feature| procedure| name| =eps~ args| name| eps name| x returns| pred| `matcher-procedure?` Like {idref =Reps~}, except that non-real values are supported and compared using magnitude. feature| library| (nitrate complex) procedure| name| rect~ args| pred| `matcher-procedure?` real~ pred| `matcher-procedure?` imag~ returns| pred| `matcher-procedure?` Matches a complex number with real part {var real~} and imaginary part {var imag~}. feature| library| (nitrate complex) procedure| name| polar~ args| pred| `matcher-procedure?` magnitude~ pred| `matcher-procedure?` angle~ returns| pred| `matcher-procedure?` Matches a complex number with magnitude {var magnitude~} and angle {var angle~}. feature| library| (nitrate rational) procedure| name| rat~ args| pred| `matcher-procedure?` numerator~ pred| `matcher-procedure?` denominator~ returns| pred| `matcher-procedure?` Matches a rational number with numerator {var numerator~} and denominator {var denominator~}. nh3| Lists and Pairs feature| procedure| name| cons~ args| pred| `tilde` car~ pred| `tilde` cdr~ returns| pred| `matcher-procedure?` Creates a matching procedure that will succeed only if the input is a pair whose car matches {var car~} and whose cdr matches {var cdr~}. The matcher for {var cdr~} is tail-called. feature| procedure| name| cons~* args| pred| `tilde` pat~{sub 1} rep| pred| `tilde` pat~{sub n} opt| pred| `tilde` tail~ returns| pred| `matcher-procedure?` Equivalent to code| pre| ({idref cons~} pat~{sub 1} ({idref cons~} pat~{sub 1} (… ({idref cons~} pat~{sub 1} tail~)))) feature| procedure| name| list~ args| rep| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Like {idref `cons~*`}, except the tail must be the empty list. feature| procedure| name| list-tail~ args| name| n pred| `tilde` pat~ returns| pred| `matcher-procedure?` Creates a matching procedure that matches the input if the input is a possibly improper list of at least {var n} elements, whose {var n}th cdr matches {var pat~}. The matching of {var pat~} occurs in tail-position. feature| procedure| name| list-ref~ args| name| n pred| `tilde` pat~ returns| pred| `matcher-procedure?` Equivalent to: code| pre| ({idref list-tail~} n (cons pat~ {idref _~})) feature| procedure| name| member-tail~ args| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Creates a matching procedure that will match the first cdr of the possibly improper input list where some cdr of the list matches {var pat~}. If there is no such cdr, the match fails. Otherwise the dictionary returned is the one returned from {var pat~}. feature| procedure| name| member~ args| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Equivalent to code| pre| ({idref member-tail~} ({idref cons~} pat~ {idref _~})) feature| procedure| name| assoc~ args| pred| `tilde` key~ opt| pred| `tilde` val~ returns| pred| `matcher-procedure?` If {var val~} is absent, defaults to {idref _~}. Equivalent to code| pre| ({idref member~} ({idref cons~} key~ val~)) feature| procedure| name| list*~ args| pred| `tilde` pat~ args| pred| `tilde` tail~ returns| pred| `matcher-procedure?` Matchs {var pat~} greedily over the elements of the input list. Once {var pat~} fails, then try to match {var tail~} on the remaining list. nh3| Vectors feature| procedure| name| vector~ args| rep| pred| `tilde` pat~ returns| pred| `matcher-procedure?` Return a matcher procedure that will succeed when the input is a vector that has the same number of elements as the arguments to this procedure. Each {var pat~} is matched to the corresponding element in the vector from left to right, passing the returned dictionaries alogn the way. The final {var pat~} is matched in tail-position. The match fails if any match fails. feature| procedure| name| vector-ref~ args| name| n pred| `tilde` pat~ returns| pred| `matcher-procedure?` Returns a matcher procedure that will succeed when the input is a vector with a length greater than {var n} and whose {var n}th element satisfies {var pat~}. The {var pat~} is matched in tail-position. nh2| Copyright © Peter McGoron 2026 Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. nh2| Index the-index|