Brzozowski derivative
The Brzozowski derivative of a regular expression with respect to a character is a regular expression that matches exactly the strings for which matches . It is computed from the syntax of by rules that resemble those of calculus, so a string can be matched by taking one derivative per character and asking at the end whether the result matches the empty string[1]. Up to a few algebraic identities, every regular expression has only finitely many derivatives, and they are the states of a deterministic finite automaton for it. Because the rules for intersection and complement are as simple as those for union, derivatives give matchers and automata for extended regular expressions with no extra machinery. Brzozowski introduced them in 1964[1]; Owens, Reppy and Turon revived them in 2009 as a practical way to build lexers[2].
Definition
For a language over an alphabet and a character , the derivative is the set of suffixes left after removing a leading :
A string is in exactly when the empty string is in . Brzozowski showed that for a language given by a regular expression the derivative is again given by a regular expression, computed by structural rules. With true when matches the empty string[1]:
where means if is nullable and otherwise. The rule for concatenation is a product rule: the first character is taken either from , or, if can be empty, from . Nullability is itself structural: and are nullable, a character is not, a concatenation is nullable when both parts are, and a union when either is.
Matching
Extended regular expressions with smart constructors, nullability and derivatives.
(* Regular expressions with intersection and complement. Alternationand intersection hold sorted lists without duplicates, so thatassociativity, commutativity and idempotence are built in. *)type t =| Empty (* matches nothing *)| Eps (* matches the empty string *)| Chr of char| Seq of t * t| Alt of t list (* r1 | r2 | ... *)| And of t list (* r1 & r2 & ... *)| Star of t| Not of t(* Smart constructors: the simplifications that keep derivatives finite. *)let seq a b = match (a, b) with| Empty, _ | _, Empty -> Empty| Eps, r | r, Eps -> r| Seq (x, y), r -> Seq (x, Seq (y, r))| _ -> Seq (a, b)let alt a b =let parts = function Alt l -> l | Empty -> [] | r -> [ r ] inmatch List.sort_uniq compare (parts a @ parts b) with| [] -> Empty| [ r ] -> r| l -> if List.mem (Not Empty) l then Not Empty else Alt llet and_ a b =let parts = function And l -> l | r -> [ r ] inmatch List.sort_uniq compare (parts a @ parts b) with| l when List.mem Empty l -> Empty| [ r ] -> r| l -> (match List.filter (( <> ) (Not Empty)) l with [] -> Not Empty | [ r ] -> r | l -> And l)let star = function Star _ as r -> r | Eps | Empty -> Eps | r -> Star rlet not_ = function Not r -> r | r -> Not r(* nu(r) holds when r matches the empty string. *)let rec nullable = function| Empty | Chr _ -> false| Eps | Star _ -> true| Seq (a, b) -> nullable a && nullable b| Alt l -> List.exists nullable l| And l -> List.for_all nullable l| Not r -> not (nullable r)(* The derivative with respect to c: the strings w such that c w matches r. *)let rec deriv c = function| Empty | Eps -> Empty| Chr d -> if c = d then Eps else Empty| Seq (a, b) -> let d = seq (deriv c a) b in if nullable a then alt d (deriv c b) else d| Alt l -> List.fold_left (fun acc r -> alt acc (deriv c r)) Empty l| And l -> List.fold_left (fun acc r -> and_ acc (deriv c r)) (Not Empty) l| Star r -> seq (deriv c r) (Star r)| Not r -> not_ (deriv c r)let matches r s = nullable (String.fold_left (fun r c -> deriv c r) r s)(* Helpers for writing expressions. *)let str s = String.fold_right (fun c r -> seq (Chr c) r) s Epslet any_of cs = List.fold_left (fun r c -> alt r (Chr c)) Empty cslet alts = List.fold_left alt Empty
Matching identifiers, a language defined by intersection and complement, and a sequence of derivatives.
open Relet letters = any_of (List.init 26 (fun i -> Char.chr (97 + i)))let digits = any_of (List.init 10 (fun i -> Char.chr (48 + i)))let sigma = star (any_of [ 'a'; 'b' ])
Running it.
identifier "x" trueidentifier "tail_rec2" trueidentifier "2fast" falseidentifier "" falseab but not ba "ab" trueab but not ba "aabb" trueab but not ba "abba" falseab but not ba "ba" falseab but not ba "aaab" trued/da (ab)* = b(ab)*d/db b(ab)* = (ab)*d/da (ab)* = b(ab)*d/db b(ab)* = (ab)*
The matcher needs no backtracking and no automaton: it keeps one expression, replaces it with its derivative at each character, and inspects nullability at the end. The derivatives of alternate between two expressions, after an and after a , which are the two states of its automaton. The language of strings containing but not is written directly with intersection and complement; classical constructions go through automata for each part and a product or subset construction.
Automata from derivatives
Brzozowski proved that a regular expression has only finitely many derivatives up to similarity, the equivalence generated by associativity, commutativity and idempotence of union[1]. Taking the distinct derivatives as states, with a transition on from to and the nullable ones as accepting states, gives a deterministic finite automaton for the expression:
Building a DFA by exploring derivatives, for the language "the (n+1)-th symbol from the end is a".
(* A DFA from derivatives: the states are the distinct derivatives of r,found by exploring every character from every state. *)open Relet dfa alphabet r =let states = Hashtbl.create 64 and queue = Queue.create () and trans = ref [] inlet id r = match Hashtbl.find_opt states r with Some i -> i | None ->let i = Hashtbl.length states in Hashtbl.add states r i; Queue.add r queue; i inignore (id r);while not (Queue.is_empty queue) dolet q = Queue.pop queue inList.iter (fun c -> trans := (id q, c, id (deriv c q)) :: !trans) alphabetdone;(Hashtbl.length states, states, List.rev !trans)(* (a|b)* a (a|b)^n: the (n+1)-th symbol from the end is an a. *)let nth_from_end n =let ab = any_of [ 'a'; 'b' ] inlet rec rep k = if k = 0 then Eps else seq ab (rep (k - 1)) inseq (star ab) (seq (Chr 'a') (rep n))
Running it.
(a|b)*a(a|b): 4 states0 --a--> 10 --b--> 01 --a--> 21 --b--> 32 --a--> 22 --b--> 33 --a--> 13 --b--> 0accepting: 2, 3n DFA states regex size (symbols)0 2 31 4 52 8 73 16 94 32 115 64 136 128 157 256 17
The expression has size linear in , and any deterministic automaton for it must remember the last symbols, so it needs states[6]. The derivative automaton has exactly that many: here it is minimal. That is not guaranteed in general, but with the simplifications of the smart constructors derivative automata are usually close to minimal, and often much smaller than the DFAs produced by the subset construction from an NFA before minimization[2].
Partial derivatives
Antimirov's partial derivatives split each derivative into a set of expressions, one per way the first character can be consumed, instead of a single union[3]. The set of partial derivatives of an expression is at most one more than its number of character occurrences, and they form the states of a nondeterministic automaton of linear size, comparable to Thompson's construction[5] and the position automaton of McNaughton and Yamada[7].
Uses
Owens, Reppy and Turon showed that lexer generators based on derivatives, with character classes as the alphabet, produce automata as small as traditional ones with simpler code, and implemented them in ML-ulex and a Scheme lexer generator[2]. Derivatives are well suited to proof assistants, since the construction is purely syntactic: Traytel and Nipkow verified decision procedures for monadic second-order logic on words based on them[8]. Z3 decides string constraints with extended regular expressions using symbolic derivatives[9], and the .NET regular expression library has a non-backtracking engine built on derivatives that runs in time linear in the input[10]. Might, Darais and Spiewak extended derivatives from regular expressions to context-free grammars, with laziness and memoization to handle recursion, giving a parser for any context-free grammar[4]; see also parser combinators.
The derivative of a regular expression is unrelated, except in name and in the shape of the product rule, to the derivative of a data type that gives the zipper, though both remove one element from the front of, or inside, a structure.
History
Brzozowski introduced derivatives of regular expressions in 1964, with the finiteness theorem and the automaton construction[1]. The method was overshadowed for decades by Thompson's NFA construction of 1968[5] and the subset construction, which suited the imperative implementations of the time. Antimirov's partial derivatives followed in 1996[3], and Owens, Reppy and Turon's 2009 paper, "Regular-expression derivatives re-examined", brought the method back into practical use[2].
see also
- LR parsingBottom-up parsing driven by a table of states: the parser shifts tokens onto a stack, and reduces the top of the stack by a grammar rule when the next token says to. A grammar for which the table cannot be built without a choice has conflicts, reported as shift/reduce or reduce/reduce.
- Parser combinatorA parser combinator is a higher-order function that builds a parser from smaller parsers: a sequence of two parsers, a choice between them, a repetition of one. Parsers are ordinary values in the host language, so a grammar is written as a program that is itself the parser, with no separate generator. Parser combinators produce recursive descent parsers; they cannot use left-recursive rules directly, and with unlimited backtracking can take exponential time, which memoization (packrat parsing) reduces to linear.
- ZipperA zipper represents a data structure together with a focus, a position inside it: the substructure at the focus, and the path from the focus back to the root together with everything to either side of that path. Moving the focus one step and editing at the focus take constant time, and the structure is persistent, so edits share everything off the path. The type of paths is the derivative of the structure's type, in the sense of calculus.
further reading
- [1]J. A. Brzozowski, “Derivatives of regular expressions”, Journal of the ACM 11 (1964).
- [2]S. Owens, J. Reppy, A. Turon, “Regular-expression derivatives re-examined”, Journal of Functional Programming 19 (2009).
- [3]V. Antimirov, “Partial derivatives of regular expressions and finite automaton constructions”, Theoretical Computer Science 155 (1996).
- [4]M. Might, D. Darais, D. Spiewak, “Parsing with derivatives: a functional pearl”, ICFP (2011).
- [5]K. Thompson, “Regular expression search algorithm”, Communications of the ACM 11 (1968).
- [6]J. E. Hopcroft, R. Motwani, J. D. Ullman, Introduction to Automata Theory, Languages, and Computation, Pearson (3rd ed., 2006).
- [7]R. McNaughton, H. Yamada, “Regular expressions and state graphs for automata”, IRE Transactions on Electronic Computers 9 (1960).
- [8]D. Traytel, T. Nipkow, “Verified decision procedures for MSO on words based on derivatives of regular expressions”, Journal of Functional Programming 25 (2015).
- [9]C. Stanford, M. Veanes, N. Bjørner, “Symbolic Boolean derivatives for efficiently solving extended regular expression constraints”, PLDI (2021).
- [10]D. Moseley, M. Nishio, J. Perez Rodriguez, O. Saarikivi, S. Toub, M. Veanes, T. Wan, E. Xu, “Derivative based nonbacktracking real-world regex matching with backtracking semantics”, PLDI (2023).
last updated