MyNixOS website logo
Description

Matchers and grammars using tree regular expressions.

t-regex: matchers and grammars using tree regular expressions

t-regex defines a series of combinators to express tree regular expressions over any Haskell data type. In addition, with the use of some combinators (and a bit of Template Haskell), it defines nice syntax for using this tree regular expressions for matching and computing attributes over a term.

Defining your data type

In order to use t-regex, you need to define you data type in a way amenable to inspection. In particular, it means that instead of a closed data type, you need to define a pattern functor in which recursion is described using the final argument, and which should instantiate the Generic1 type class (this can be done automatically if you are using GHC).

For example, the following block of code defines a Tree' data type in the required fashion:

{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics

data Tree' f = Leaf'
             | Branch' { elt :: Int, left :: f, right :: f }
             deriving (Generic1, Show)

Notice the f argument in the place where Tree' would usually be found. In addition, we have declared the constructors using ' at the end, but we will get rid of them soon.

Now, if you want to create terms, you need to close the type, which essentially makes the f argument refer back to Tree'. You do so by using Fix:

type Tree = Fix Tree'

However, this induces the need to add explicit Fix constructors at each level. To alleviate this problem, let's define pattern synonyms, available from GHC 7.8 on:

pattern Leaf = Fix Leaf'
pattern Branch n l r = Fix (Branch' n l r)

From an outsider point of view, now your data type is a normal one, with Leaf and Branch as its constructors:

aTree = Branch 2 (Branch 2 Leaf Leaf) Leaf

Tree regular expressions

Tree regular expressions are parametrized by a pattern functor: in this way they are flexible enough to be used with different data types, while keeping our loved Haskell type safety.

The available combinators to build regular expressions follow the syntax of Tree Automata Techniques and Applications, Chapter 2.

Emptiness

The expressions empty_ and none do not match any value. They can be used to signal an error branch on an expressions.

Whole language

You can match any possible term using any_. It is commonly use in combination with capture to extract information from a term.

Choice

A regular expression of the form r1 <||> r2 tries to match r1, and if it not possible, it tries to do so with r2. Note than when capturing, the first regular expression is given priority.

Injection

Of course, at some point you would like to check whether some term has a specific shape. In our case, this means that it has been constructed in some specific way. In order to do so, you need to inject the constructor as a tree regular expression. When doing so, you can use the same syntax as usual, but where at the recursive positions you write regular expressions again.

Let's make it clearer with an example. In our initial definition we had a constructor Branch' with type:

Branch' :: Int -> f -> f -> Tree' f

Once you inject it using inj, the resulting constructor becomes:

inj . Branch' :: Int -> Regex' c f -> Regex c' f -> Tree' (Regex' c f)

Notice that fields with no f do not change their type. Now, here is how you would represent a tree whose top node is a 2:

topTwo = inj (Branch' 2 any_ any_)

In some cases, you don't want to check the value of a certain position which is not recursive. In that case, you cannot use any_, since we are not talking about the type being built upon. For that case, you may use the special value __.

For example, here is how you would represent the shape of a tree which has at least one branch point:

someBranching = inj (Branch' __ any_ any_)

Iteration and concatenation

Iteration in tree regular expressions is not as easy as in word languages. The reason is that iteration may occur several times, and in different positions. For that reason, we need to introduce the notion of hole: a hole is a placeholder where iteration takes place.

In t-regex hole names are represented as lambda-bound variables. Then, you can use any of the functions square, var or # to indicate a position where the placeholder should be put. Iteration is then indicated by a call to iter or its post-fix version ^*.

The following two are ways to indicate a Tree where all internal nodes include the number 2:

{-# LANGUAGE PostfixOperators #-}

regex1 = Regex $
           iter $ \k ->
                  inj (Branch' 2 (square k) (square k))
             <||> inj Leaf'

regex2 = Regex $ ( (\k -> inj (Branch' 2 (k#) (k#)) <||> inj Leaf')^* )

Notice that the use of PostfixOperators enables a much terse language.

Iteration is an instance of a more general construction called concatenation, where a hole in an expression is filled by another given expression. The general shape of those are:

(\k -> ... (k#) ...) <.> r  -- k is replaced by r in the first expression

Matching and capturing

You can check whether a term t is matched by a regular expression r by simply using:

r `matches` t

However, the full power of tree regular expression come at the moment you start capturing subterms of your tree. A capture group is introduced in a expression by either capture or <<-, and all of them appearing in a expression should be from the same type. For example, we can refine the previous regex2 to capture leaves:

regex2 = Regex $ ( (\k -> inj (Branch' 2 (k#) (k#)) <||> "leaves" <<- inj Leaf')^* )

To check whether a term matches a expression and capture the subterms, you need to call match. The result is of type Maybe (Map c (m (Fix f))). Let's see what it means:

  • The outermost Maybe indicates whether the match is successful or not. A value of Nothing indicates that the given term does not match the regular expression,
  • Capture groups are returned as a Map, whose keys are those given at capture or <<-. For that reason, you need capture identifiers to be instances of Ord,
  • Finally, you have a choice about how to save the found subterms, given by the Alternative m. Most of the time, you will make m = [], which means that all matches of the group will be returned as a list. Other option is using m = Maybe, where only the first match is returned.

Tree regular expression patterns

Capturing is quite simple, but comes with a problem: it becomes easy to mistake the name of a capture group, so your code becomes quite brittle. For that reason, t-regex includes matchers which you can use at the same positions where pattern matching is usually done, and which take care of linking capture groups to variable names, making it impossible to mistake them.

To use them, you need to import Data.Regex.TH. Then, a quasi-quoter named rx is available to you. Here is an example:

{-# LANGUAGE QuasiQuotes #-}

example :: Tree -> [Tree]
example [rx| (\k -> inj (Branch' 2 (k#) (k#)) <||> leaves <<- inj Leaf')^* |]
           = leaves
example _  = []

The name of the capture group, leaves, is now available in the body of the example function. There is no need to look inside maps, this is all taken care for you.

Note that when using the rx quasi-quoter, you have no choice about the Alternative m to use when matching. Instead, you always get as value of each capture group a list of terms.

For those who don't like using quasi-quotation, t-regex provides a less magical version called with. In this case, you need to introduce the variables in a explicit manner, and then pattern match on a tuple wrapped inside a Maybe. The previous example would be written as:

{-# LANGUAGE ViewPatterns #-}

example :: Tree -> [Tree]
example with (\leaves -> Regex $ iter $ \k -> inj (Branch' 2 (k#) (k#))
                                              <||> leaves <<- inj Leaf' )
             -> Just leaves
           = leaves
example _  = []

Notice that the pattern is always similar with (\v1 v2 ... -> regular expression) -> Just (v1,v2,...).

Random generation

You can use t-regex to generate random values of a type which satisfy a certain tree regular expression. Of course, you might always generate random values and then check that they match the given expression, but this is usually very costly and maybe even statistically impossible. Instead, you should use arbitraryFromRegex.

instance Arbitrary Tree where
  arbitrary = frequency
    [ (1, return Leaf)
    , (5, Branch <$> arbitrary <*> arbitrary <*> arbitrary) ]

> arbitraryFromRegex regex2

Note that in the previous example we also gave an instance declaration for Arbitrary. This class comes from the QuickCheck package, and is needed to generate unconstrained random values for the case in which any_ is found.

Sometimes you may not be able or want to write such an instance. In that case, you can use arbitraryFromRegexAndGen, which takes an additional argument from which any_ values are generated.

Attribute grammars

Attribute grammars are a powerful way to perform computations over a term. The main idea is that each node in your term (when seen as a tree) is traversed, and two sets of information are recorded at each point:

  • Inherited attributes go from parent to children. When describing a grammar, each node needs to specify the value of inherited attributes of all their children,
  • Synthesized attributes flow in the other direction. At each node, you need to described how to get the value of each synthesized attribute based on your inherited attributes and the synthesized attributes of children.

The most performant attribute grammar compilers, such as UUAGC only allow deciding which rule to apply on a node depending on their topmost constructor. With t-regex you can look as deep as you want to take this decision (but of course, performance will suffer if you do this very often).

Here is an example of a grammar which computes a graphical representation of a Tree plus its number of leaves:

grammar = [
    rule $ \l r ->
     inj (Branch' 2 (l <<- any_) (r <<- any_)) ->> do
       (lText,lN) <- use (at l . syn)
       (rText,rN) <- use (at r . syn)
       this.syn._1 .= "(" ++ lText ++ ")-SPECIAL-(" ++ rText ++ ")"
       this.syn._2 .= lN + rN
  , rule $ \l r ->
     inj (Branch' __ (l <<- any_) (r <<- any_)) ->>> \(Branch e _ _) -> do
	   check $ e >= 0
       (lText,lN) <- use (at l . syn)
       (rText,rN) <- use (at r . syn)
       this.syn._1 .= "(" ++ lText ++ ")-" ++ show e ++ "-(" ++ rText ++ ")"
       this.syn._2 .= lN + rN
  , rule $ inj Leaf' ->> do
       this.syn._1 .= "leaf"
       this.syn._2 .= Sum 1
  ]

Let's dissect it part by part.

First of all, a grammar is made of a series of rules. Each rule follows the same schema:

rule $ \v1 ... vn ->
  regular expression ->> do
    actions

rule is the constant part which prefixes every rule. Then, you have the set of variables which will be used to capture information from the term, in a similar way to previous section. After that you have the tree regular expression the term needs to match. Finally, and separated by ->>, you find the actions to perform when this rule is selected.

Two small extensions are shown in the second rule. By default, ->> does not give you access to the matched term. However, if you need to access some of its information (for example, because you used __, as in this case), you can use the alternative version ->>> which gives this as an argument. The second extension is the use of check to pinpoint a logical condition which is not captured by the regular expression itself.

The syntax for the actions relies heavily in lenses and operators from the lens package. In particular, you have four lenses:

  • this.inh gives access to the inherited attributes of the node,
  • at n . syn gives access to the synthesized attributes of children,
  • this.syn is where you set the synthesized attributes of your node,
  • at n . inh is where you set the inherited attributes of children,

Furthermore, you can combine those with lenses over your inherited and synthesized attribute data type to have more lightweight syntax. In our case, the synthesized attributes are a tuple (String, Sum Int), so we access them with _1 and _2, as shown in the example.

The general rule is that you read values using use, and set values via .=. If some value is not set, it defaults to the empty element of your synthesized type, or to the value of parent node in the case of inherited attributes.

Metadata

Version

0.1.0.0

Platforms (77)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-freebsd
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64-windows
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows