The New Rewriting Engine of Dedukti

Dedukti is a type-checker for the $\lambda$$\Pi$-calculus modulo rewriting, an extension of Edinburgh's logicalframework LF where functions and type symbols can be defined by rewrite rules. It thereforecontains an engine for rewriting LF terms and types according to the rewrite rules given by the user.A key component of this engine is the matching algorithm to find which rules can be fired. In thispaper, we describe the class of rewrite rules supported by Dedukti and the new implementation ofthe matching algorithm. Dedukti supports non-linear rewrite rules on terms with binders usinghigher-order pattern-matching as in Combinatory Reduction Systems (CRS). The new matchingalgorithm extends the technique of decision trees introduced by Luc Maranget in the OCamlcompiler to this more general context.


Introduction
Dedukti is primarily a type-checker for the so-called λΠ-calculus modulo rewriting, λΠ/R, an extension of Edinburgh's logical framework LF [9] where function and type symbols can be defined by rewrite rules.This means that Dedukti takes as input type declarations and rewrite rules, and check that expressions are well typed modulo these rewrite rules and the β-reduction of λ-calculus.
The λΠ-calculus is the simplest type system on top of the pure untyped λ-calculus combining both the usual simple types of (functional) programming (e.g. the type N → N of functions from natural numbers to natural numbers) with value-dependent types (e.g. the type Πn : N, V (n) of vectors of some given dimension).In fact, a simple type A → B is just a particular case of dependent type Πx : A, B where x does not occur in B. Syntactically, this means that types are not defined prior to terms as usual, but that terms and types are mutually defined.
Moreover, in λΠ/R, a term of type A is also seen as a term of type B if A and B are equivalent not only modulo β-reduction but also modulo some user-defined rewrite rules R. Therefore, to check that a term t is of type A, one has to be able to check when two

12:2
The New Rewriting Engine of Dedukti expressions are equivalent modulo β-reduction and rewrite rules.This is why there is a rewriting engine in Dedukti.
Thanks to the Curry-Howard correspondence between λ-terms and proofs on the one hand, and (dependent) types and formulas on the other hand, Dedukti can be used as a proof checker.Hence, in recent years, many satellite tools have been developed in order to translate to Dedukti proofs generated by automated or interactive theorem provers: Krajono for Matita, Coqine for Coq, Holide of OpenTheory (HOL Light, HOL4), Focalide for Focalize, Isabelle, Zenon, iProverModulo, ArchSAT, etc. [1].
By unplugging its type verification engine to only retain its rewriting engine, Dedukti can also be used as a programming language.Thanks to its rewriting capabilities, Dedukti can be used to apply transformation rules on terms and formulas with binders [18,4].
A rewrite rule is nothing but an oriented equation [2].Rewriting consists in applying some set of rewrite rules R (and β-reduction) as long as possible so as to get a term in (weak head) normal form.At every step it is therefore necessary to check whether a term matches some left-hand side of a rule of R. It is therefore important to have an efficient algorithm to know whether a rule is applicable and select one: Example 1.Consider the following rules in the new Dedukti syntax (pattern variables are prefixed by $ to avoid name clashes with other symbols): To select the correct rule to rewrite a term, the naive algorithm matches the term against each rule left-hand side from the top rule to the bottom one.Let us apply the algorithm on the matching of the term t = f (c (c e)) b.
The first argument of t is matched against the first argument of the first left-hand side c (c $x).As c (c e) matches c (c $x), it succeeds.However, when we pass to the second argument, b does not match the pattern a.So the second rule is tried.Pattern $x filters successfully c (c e), and b matches b, so it succeeds.
Yet, matching c (c e) against c (c $x) can be avoided.Indeed, if we start by matching the second argument of f, then the first rule is rejected in one comparison.The only remaining work is to match c (c e) against $x.
In [14], Maranget introduces a domain-specific language of so-called decision trees for describing matching algorithms, and a procedure for compiling some set of rewrite rules into this language.But his language and compilation procedure handle rewrite systems whose left-hand sides are linear constructor patterns only.In Dedukti, as we are going to see it soon, we use a more general class of patterns containing defined symbols and λ-abstractions.They can also be non-linear and contain variable-occurrence conditions as in Klop's Combinartory Reduction Systems (CRS) [11].
In this paper, we describe an extension of Maranget's work to this more general setting, and present some benchmark.

Outline of the paper
In Section 2, we start by giving examples of the kind of rewrite rules that can be handled by Dedukti, before giving a more formal definition.In Section 3, we present our extension of Maranget's decision trees, their syntax and semantics, and how to compile a set of rewrite rules into this language.In Section 4, we compare this new implementation with previous ones and other tools implementing rewriting.Finally, in Section 5, we discuss some related work and conclude.

Rewriting in Dedukti
We will start by providing the reader with various examples of rewrite rules accepted by Dedukti before giving a formal definition.To this end, we will use the new Dedukti syntax (see https://github.com/Deducteam/lambdapi).In this new syntax, one can use Unicode characters, some function symbols can be written in infix positions and, in rewrite rules, pattern variables need to be prefixed by $ to avoid name clashes with function symbols.Note however that, for the sake of simplicity, we may omit some declarations.
Dedukti can of course handle the "Hello world!" example of first-order rewriting, the addition on unary natural numbers, as follows: symbol : TYPE symbol 0: symbol s : More interestingly is the fact that, in constrast to functional programming languages like OCaml, Haskell or Coq, rule left-hand sides can overlap each other.Consequently, in Dedukti, addition on unary numbers can be more interestingly defined as follows: With the first definition, one has 0 + t equivalent to t modulo rewriting, written 0 + t t, for all terms t (of type N), but not t + 0 t.Hence, the interest of the second definition.
It is also possible to match on defined symbols and not just on constructors like in usual functional programming languages.Hence, for instance, one can add the following associativity rule on addition: Moreover, one can use non-linear patterns, that is, require the equality of some subterms to fire a rule like in: Therefore, Dedukti can handle any first-order rewriting system [2].But it can also handle higher-order rewriting in the style of Combinatory Reduction Systems (CRS) [11].
The simplest example of higher-order rewriting is given by the map function on lists, which applies an argument function to every element of a list: Unlike first-order rewriting, function symbols can be partially applied, including in patterns.Hence, in Dedukti, one can write the following: The New Rewriting Engine of Dedukti It is also possible to match λ-abstractions as follows: symbol cos : symbol sin : symbol *: ( ) ( ) ( ) symbol diff : ( ) ( ) Following the definition of CRS, in a rule left-hand side, a higher-order pattern variable can only be applied to distinct bound variables (this condition could be slightly relaxed though [13]).A similar condition appears in λProlog [15].It ensures the decidability of matching.
It can also be used to check variable-occurrence conditions.The differential of a constant function can thus be simply defined as follows in Dedukti: While in the rule for sin, we had $v [x], meaning that the term matching $v [x] may depend on x, here we have $v applied to no bound variables, meaning that the term matching $v cannot depend on x.

Terms, Patterns, Rewrite Rules and Matching, Formally
We now define more formally terms, patterns, rewrite rules and rewriting.Following [3], the terms of the λΠ-calculus are inductively defined as follows: where x is a term variable, f is a function symbol, tu is the application of the function t to the term u, λx : t, u is the function mapping x of type t to u, which type is the dependent product Πx : t, u.The simple type t → u is syntactic sugar for Πx : t, u where x is any fresh term variable not occurring in u.
In λx : t, u and Πx : t, u, the occurrences of x in u are bound, and terms equivalent modulo renaming of their bound variables are identified, as usual.In Dedukti, this is implemented by using the Bindlib library [12].
A (possibly empty) ordered sequence of terms t 1 , . . ., t n is written t for short.Patterns are inductively defined as follows: where $x is a pattern variable and y is a sequence of distinct bound variables.
A rewrite rule is a pair of terms, written → r, such that is a pattern of the form f p and every pattern variable occurring in r also occurs in .
In the following, we will assume given a set of user-defined rewrite rules R. Matching a term t against a pattern p whose bound variables are in the set V , written p V t is inductively defined as follows: and we say that the term t matches the pattern p or that the pattern p filters the term t.
The indexing set V of variables is used to record which binders have been traversed, which is necessary to perform variable-occurrence tests.
The condition in the (MatchTuple) rule translates non-linearity conditions: if a variable occurs twice in a pattern, then the matching values must be equal.

Implementing Matching With Decision Trees
The rewriting engine described in this paper is based on the work of Maranget [14].Maranget introduces a domain-specific language for matching and an algorithm to transform a (ordered) list of first-order linear constructor patterns into a program in this language.In this section, we explain how we extend Maranget's language and compilation procedure to our more general setting with non-linear higher-order patterns, partially applied function symbols, and no order on patterns.We start by defining the language of decision trees D and switch case lists L: where r is a rule right-hand side, i, j and n are integers, X is a finite set of variables.For case lists, s is a function symbol annotated with the number of arguments it is applied to, :: is the cons operator on lists and nil is the empty list.
An element of a switch case list is a pair mapping: a function symbol s to a tree for matching its arguments, a λ to a tree for matching the body of an abstraction, a default case * to a tree for matching the other arguments.Note that a list L λ has no element (λ, D) and, in a list L, there is at most one element of the form (λ, D).Finally, in both cases, there is at most one element of the form ( * , D) and, if so, it is the last one (default case).
Semantics Decision trees are evaluated along with a stack of terms v to filter and an array s used by the decision tree to store elements.Informally, the semantics of each tree constructor is as follows: Leaf(r) matching succeeds and yields right-hand side r.Fail matching fails.Swap i (D) moves the ith element of v to the top of v and carries on with D. Store(D) stores the top of the stack into s and continues with D. Switch(L) branches on a tree in L depending on the term on top of v. BinNl(D, {i, j}, E) checks whether s i and s j are equal and continues with D if this is the case, and E otherwise.BinCl(D, (n, X), E) checks whether FV(s n ) ⊆ X and continues with D if this is the case, and with E otherwise.
Example 2. The matching algorithm described in Example 1 can be represented by the following decision tree: rewrite rules and a path from the root to a leaf is a successful matching.The tree of Figure 1 can be used to rewrite any term of the form f t.The sequence of operations to filter the term f (f a) b can be read from the tree.The initial vector v is v = (f a, b) and the array s won't be necessary here.
1.The Swap 2 transforms v into (b, f a), so the next operations will be carried out on b.

3.
Node E is in fact a Leaf and so the matching succeeds.
Note that the top symbol f is not matched.Top symbols are analysed prior to filtering as they are needed to get the appropriate decision tree to filter the arguments.
The formal semantics is given in Figure 2.An evaluation is written as a judgement v, s, V D r which can be read: "stack v, store s and abstracted variables V yield the term r when matched against tree D".We overload the comma notation, using it for the cons (s, v) and the concatenation ( v, w).The | is used as the alternative.
Matching succeeds with the Match rule.Terms are memorised on the stack s using the Store rule.Matching on a symbol is performed with the SwitchSymb rule.If the stack has a term f applied on top and the switch-case list L contains an element (f, D), then the symbol f can be removed, and matching continues using sub-tree D. The rule SwitchDefault allows to match on any symbol or abstraction, provided that the switch-case list L has a default case (and that we can apply neither rule SwitchSymb nor SwitchAbst).The binary constraint rules guide the matching depending on failure or success of the constraints.The last three rules allow to search for a symbol in a switch-case list.A judgement s L p reads "looking for symbol s in list L yields pair p".Cont skips a cell of the list, Default returns unconditionally the default cell of the list (which is the last by construction) and Found returns the cell that matches the symbol looked for.

Matrix Representation of Rewrite Systems
In order to compile a set of rewrite rules into this language, it is convenient to represent rewrite systems as tuples containing a matrix and three vectors.The matrix contains the patterns and can have lines of different lengths because function symbols can be partially applied.The vectors contain the right-hand side of the rewriting system and the constraints.Hence, a rewrite system for a function symbol f , that is, a set of rewrite rules f p 1  The New Rewriting Engine of Dedukti is represented by: where C i encodes the variable-occurrence constraints in p i and N i encodes non-linearity constraints in p i .A variable-occurrence constraint given by a pattern variable $x [ y] is encoded as a pair (a, y) where a is the position of the variable in the main term.
Non-linearity constraints between two terms at positions a and b are encoded by the unordered pair {a, b}.
In the above matrix, we can then replace a pattern of the form $x [ y] or $x by _.
For the sake of completeness, we recall the definition of positions: Definition 3 (Positions in a term).The set of positions of a term t is the set of words over the alphabet of positive integers inductively defined as follows: The position is called the root position of the term t and the symbol at this position is called the root symbol of t.
For a ∈ Pos(t), the subterm of t at position a, denoted by t| a , is defined by induction on the length of a, t| t and f t 1 is represented by the following matrix: The variable-occurence constraint of the first rule is encoded by (211, (x)) since only the variable x is authorised in $g [x].The non-linearity constraint f &x &x is translated by {1, 2}, hence the constraints set {{1, 2}}.

Compiling Rewrite Systems to Decision Trees
We will describe the compilation process as a non-deterministic recursively defined relation between matrices and decision trees.
To this end, we use the transformations on matrices defined in Table 1.
Spec f, a, P, N , C, r keeps rows whose first pattern filters the application of function f a arguments:

12:9
Pattern p j 1 Rows of Spec(f, a, P → A) Rows of Spec λ (P → A) Rows of Def(P → A) Spec λ P, N , C, r keeps rows whose first pattern filters a λ-abstraction: Example 6.Let P be the same as in Example 5.
Def P, N , C, r keeps rows whose first pattern is a pattern variable: Example 7. Let P be the same as in Example 5.

Def P, N , C, r = r , N , C, r 3
To sum up, given a pattern matrix P , a simplification function removes rows of P that are not compatible with some assumption on the form of the first pattern.The same idea is used for constraints.Note that we will abuse set notations and write k ∈ N or N \{k} even if N is not a set of elements of the type of k.In that case k ∈ N is false and N \{k} is N .csucc(k, (P, N , C, r)) keeps all the rows and simplify the constraint sets csucc(k, ( p, N, C, r)) ( p, N \{k}, C\{k}, r) cfail(k, (P, N , C, r)) keeps rows that don't have k in their constraint sets A compilation process consists in reducing the matrix step by step, compiling the submatrices and aggregating the sub-trees obtained using the node that corresponds to the computed sub-matrices (e.g. a Switch if the Spec, Def and Spec λ sub-matrices have been computed).
To say that the matrix P, N , C, r compiles to the decision tree D, we write ρ, P, N , C, r , n, E  D where: F S C D 2 0 2 0 12:10 The New Rewriting Engine of Dedukti ρ are the positions in the term that will be matched against during evaluation.E is a map from positions to integers such that E(ρ) is the index in s of the subterm at position ρ used during the evaluation of decision trees.The empty map is denoted ∅. n is the size of the store, which is incremented each time an element is added.
We now describe the compilation process implemented in Dedukti: Definition 8 (Compilation). 1.If the matrix P has no row (m = 0), then matching always fails, since there is no rule to match, 2. If there is a row k in P composed of unconstrained variables, matching succeeds and yields right-hand side r

Otherwise, there is at least one row with either a symbol or a constraint or an abstraction.
We can choose to either specialise on a column or solve a constraint.a.Consider a specialisation on the first column, assuming it contains at least a symbol or an abstraction.If ρ 1 is constrained in some N i or C i , then define n = n + 1 and E = E ∪ {ρ 1 → n}.
Otherwise, let n = n and E = E. Let Σ be the set of root symbols of the terms of the first column and k the number of arguments f is applied to.Then for each f ∈ Σ, we compile Let L be the switch case list defined as (we use the bracket notation for list comprehension as the order is not important here) If there is an abstraction in the column, the Spec λ sub-matrix is computed and compiled to D λ , and an abstraction case is added to the mapping Similarly, if the column contains a variable, the Def sub-matrix is computed and compiled to D * , and the mapping is completed with a default case, (the abstraction case may or may not be present) Now that the switch case list L is complete (all the symbols, the abstractions and the pattern variables are handled) and the sub-trees are defined and related to their pattern matrix, we can create the top node Switch(L).Furthermore, if ρ 1 is constrained, the term must be saved during evaluation.In that case, we add a Store node, ρ, P, N , C, r , n, E Store(Switch(L)). Otherwise, b.If a term has been stored and is subject to a closedness constraint, then this constraint can be checked.That is, for any position µ such that E(µ) is defined and there is a constraint set C i and a variable set V such that (µ, V ) ∈ C i , we compute the sub-matrices csucc and cfail and we compile them to D s and D f , with (µ, X) ∈ F j for some row number j and we finally define A non linearity constraints can be enforced when the two terms involved in the constraint have been stored, that is, when there is a couple {µ, ν} (µ = ν) such that E(µ) and E(ν) are defined and there is a row j such that {µ, ν} ∈ N j .If it is the case, then compute csucc, cfail and compile them, If column i contain either a symbol, an abstraction or a constraint, and each pattern vector of P is at least of length i, then compile µ, P , N , F , r , n, E D where µ = (ρ i ρ 1 . . .ρ n ) and P is P with column i moved to the front; to build Example 9 (Example 1, 2 continued).We consider again the rewriting system used in Example 1.We start by computing the matrices: 1. We saw that it is better to start examining the second argument, so we start by swapping columns of the matrix, P = a c (c _) b _ , define D such that (2 1), (P , ∅, ∅, r) , 0, ∅ D.

3.
Since P b contains only unconstrained variables, we are in the case item 2 and so we have (1, (P b , ∅, ∅, $x) , 0, ∅) Leaf($x).4. A specialisation on P a with respect to c can be performed, let Q a Spec(c, 0, P a ) = c _ and define E such that (1, (Q a , ∅, ∅, $x) , 0, ∅) E. The compilation step produces 5. Similarly, we can specialise Q a on c yielding the matrix _ which compiles to Leaf.We thus have, The soundness and completeness proofs for this compilation process can be found in [10].
We have seen that at each compilation step, several options are possible.The stack can be swapped with Swap to orient the filtering.If a constraint can be solved, either is is solved with a BinNl or BinCl node, or a Switch can be performed.These possibilities make the compilation process undeterministic.Therefore, a given matrix can be compiled to several decision trees.Maranget compares different heuristics based on the shape of patterns as well as some more complex ones.In Dedukti, since verifying constraints can involve non trivial operations (non-linearity and variable occurrence tests), constraint checking is postponed as much as possible.Regarding Swap, we process in priority columns that have many constructors and few constraints.

Results
This section compares the performance of the new rewriting engine with previous implementations of Dedukti, and other tools as well.

Hand-written examples
We consider 3 different implementations of Dedukti: Dedukti2.6 is the latest official release of Dedukti available on opam.Its matching algorithm also implements decision trees but non-linearity and variable-occurrence constraints are not integrated in decision trees.Its implementation, primarily due to Ronan Saillard [17], is available on https://github.com/Deducteam/dedukti.
ok_50x80 contains a formula with 50 literals and 80 clauses of the form ¬x ∨ ¬y ∨ ¬z.Because of the nature of the problems, they require a substantial amount of rewriting steps to be solved.
Table 2 shows the performance of each tool on these examples.Using decision trees increases significantly performance on Sudoku since Lambdapi1.0 is twice as slow as De-dukti2.6which is slower than Dedukti3.0.SAT problems confirm that Dedukti3 is more efficient than Lambdapi1.0and Dedukti2.
More benchmarks are described in [10].

Rewriting Engine Competition (REC)
The Rewriting Engine Competition 1 (REC), first organized in 2009, aims to compare rewriting engines.F. Duràn and H. Garavel revived the competition in 2018, and another study has been done in 2019 [5].There are 14 rewriting engines tested, among which Haskell's GHC and OCaml.REC problems are written in a specific REC syntax which is then translated into one of the 14 target languages with Awk scripts.To use REC benchmarks with Dedukti, a translation from Haskell benchmarks to Dedukti has been implemented 2 .
Our rewriting engine 3 has been compared on the problems that do not use conditional rewriting with OCaml and Haskell.For each language, we have measured both the interpretation time with ocaml and runghc, and the compiling and running time with ocamlopt and ghc.The results are in Table 3.We can divide our observations on classes of problems.There are 43 problems, among which 22 are solved in less that one second by at least two other solvers than Dedukti (the first group of the table).On these problems, our rewriting engine is in average 4 times faster than the median of the other rewriting engines.The second group contains problems on which no other tool than Dedukti needs more than ten seconds.On this group, Dedukti is in average 10 times slower than the median of the other tools.However, Dedukti performs better than interpreted OCaml on add8 and better than compiled OCaml onbenchtree10.On the last group, Dedukti is in average 60 times slower than other engines.Interestingly ocamlopt has more memory overflows than Dedukti (seven against four).

Conclusion & Related Work
This article describes the implementation of the new rewriting engine of Dedukti.It extends Maranget's techniques of decision trees used in the OCaml compiler [14] to the class of non-linear higher-order patterns used in Combinatory Reduction Systems (CRS) [11].We define the language of decision trees and how to compile a set of rewrite rules into a decision tree.We finally present some benchmarks showing good performances.A similar algorithm had been implemented in Dedukti2.6 by Ronan Saillard [17].However, Saillard's rewriting engine used decision trees for first-order linear matching and handled non-linearity and variable-occurrence constraints afterwards in a naive way.In the new implementation, these constraints are fully integrated in decision trees.
Other rewriting engine uses decision trees as well like CRSX, which is a rewrite engine for an extension of Combinatory Reduction Systems [16], and Maude under certain conditions [8], but Maude considers first-order terms only.
Other pattern-matching algorithm are also possible, in particular using backtracking automata instead of trees, which allow to have smaller data structures.The interested reader can look at Prolog implementations or Egison (see [6] and, more particularly on the question of pattern matching, [7]).
Further useful extensions would be interesting: conditional rewrite rules (the REC database contains many files with conditional rewrite rules) and matching modulo associativity and commutativity (AC).Conditional rewriting could be implemented without too much difficulty since it would consist in extending the constraints mechanism which is modular.A prototype implementation of matching modulo AC has already been developed for De-dukti2.6 by Gaspard Férey4 but performance is not very good yet.This new implementation could provide a better basis to implement matching modulo AC.

F
symbol id : rule id $x $x rule plus 0 id with plus ( s $n ) $m s ( plus $n $m ) rule map id $l $l

FFigure 1
Figure 1 Graphical representation of the decision tree of Example 2 Switch node with the case list [(a 0 , D), (b 0 , E)] allows to branch on D or E depending on the term on top of v, that is, b.Since b is applied to no argument, it matches b 0 and filtering continues on E. The stack is now v = (f a).

1F
http://rec.gforge.inria.fr 2 file tools/rec_to_lp/rec_hs_to_lp.awkavailable from revision e8388b73 (published on May 12, 2020)3 with revision a0009fdaa (published on Jan.10, 2020) The notion of position is extended to sequences of terms by taking t ia t i | a .
• • • t n | ia t i | a

Table 2
[12] needed to solve Sudoku and SAT formulae in seconds.Lambdapi1.0 is an alternative implementation of Dedukti due to Rodolphe Lepigre[12].It implements a naive algorithm for matching.It is available on https://github.com/rlepigre/lambdapi/tree/fix_ho.Dedukti3.0 is our new implementation of Dedukti.It adds to Lambdapi1.0 the decision trees described in this paper.It is available on https://github.com/Deducteam/lambdapi.The git repository https://github.com/deducteam/librariescontains several handwritten Dedukti examples, including a Sudoku solver with 3 examples labelled easy, medium and hard respectively, and a DPLL-based SAT solver to decide the satisfiability of propositional logic formulae in conjunctive normal form with two example files: 2_ex contains a function that when given a integer n, produces n literals named v n and the formula p

Table 3
Performance on REC benchmark in seconds.N/A is for out of memory.T/O is for timeout (30 minutes).The last line indicates that on the langton7 problem, Dedukti ran out of memory, the command runghc langton7.hstook 533.2 seconds to finish, ocaml langton7.mltook 101.7 seconds, ghc langton7.hs&& ./langton7took 66 seconds and ocamlopt langton7.ml&& ./a.out took 39.6 seconds.