A regex engine built around Brzozowski derivatives, following the
"A Play on Regular Expressions"
functional pearl (Fischer, Huch, Wilke), extended with a Semiring
abstraction so the same matching logic can compute a Bool
(accept/reject), an Int (count of matching derivations), or a
submatch position via the Leftmost and LeftLong semirings.
No dependencies beyond base.
Reg/accept— a naive, exponential-time reference implementation via direct case splitting on the string.REG/match— an efficient version based on Brzozowski derivatives (shift), linear in the length of the input for a fixed pattern.parseRegparses a small regex syntax (literals,|,*, parenthesized grouping) into aREGvia a recursive-descent parser with standard precedence (*binds tightest, then concatenation, then|).REGw/matchw— the same derivative algorithm generalized over an arbitrarySemiring, so weighted variants (submatch position tracking, counting derivations, etc.) share one implementation with plain boolean matching.submatchwsearches a longer string for the leftmost (or leftmost-longest) match of a pattern, using theLeftmost/LeftLongsemirings to track position.
parseRegalternation bug. The original tokenizer only split the input on(/), so|or*occurring right next to a literal — rather than next to a parenthesis — stayed glued to it as a single token ("(ab)|c"produced a garbled tree, while"(ab)|(c)"worked). Replaced with a recursive-descent parser operating directly on the string, which has no such gap.LeftLong's(<+>)crash. Itsleftlonghelper only handled the case where both operands were already aRange, missing theNoRangecases that the analogousLeftmostinstance already handled correctly. Any pattern containing aRepwhose body can match the empty string (e.g.(a|b)*) producedLeftLong NoRangevalues during the fold, which crashed with "Non-exhaustive patterns in function leftlong". BothNoRange/NoRangeand mixedNoRange/Rangecases are now handled, mirroringLeftmost.
Both are covered by regression tests in test/Spec.hs.
submatchw, matchw, and the values built from symi (a, ab,
aaba, aib, ...) are polymorphic over the Semiring (and, for
symi, Semiringi) type class, with no functional dependency tying
that type to anything else in the call. GHC can't infer it from
context alone — an explicit annotation is required at the call site:
submatchw ab "xaby" :: LeftLong -- required, not just cosmeticOmitting it produces an "Ambiguous type variable" error at compile time, not a runtime failure — this is normal Haskell type-class behavior, not a bug in the library.
cabal build
cabal run playreg-demo # a short tour of the library, printed to stdout
cabal test # 17 regression testsOr directly with GHC (no dependencies beyond base, so no package
setup needed):
ghc -isrc -iapp -o playreg-demo app/Main.hs
./playreg-demo
ghc -isrc -itest -o playreg-test test/Spec.hs
./playreg-testplayreg/
├── playreg.cabal
├── src/PlayReg/RegExp.hs -- the library
├── app/Main.hs -- demo executable
└── test/Spec.hs -- 17 regression tests
BENEVIDES, Marcos Vinicius Moreira Serra. Implementação e análise de uma engine para expressões regulares em Coq via testes baseados em propriedades. 2019. Trabalho de Conclusão de Curso (Ciência da Computação) — Universidade Federal do Maranhão, São Luís, 2019. Disponível em: http://hdl.handle.net/123456789/3952.