Smirk (;-P / ;-) — Pure Native Resumable PCRE Engine in Haskell
Smirk is a 100% native Haskell Perl-Compatible Regular Expression (PCRE) engine. It provides high-assurance, pure functional regular expression matching, step-bounded ReDoS immunity, pausable/resumable continuations, sequence polymorphism, and first-class algebraic effect handlers without C dependencies, buffer allocations, or FFI overhead.
The Name & Emoticon Grounding
The library's name and symbol—Smirk (;-P / ;-))—express cheeky, playful defiance:
- In the security and system reliability world, regular expression engines are notorious for disastrous catastrophic backtracking—the dreaded ReDoS (Regular Expression Denial of Service) attack, where a pathological string like
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa! against (a+)+$ locks a CPU core in exponential \(\mathcal{O}(2^N)\) backtracking for hours or days.
- In mainstream Haskell, developers were traditionally forced to link against foreign C libraries (
libpcre / libpcre2) via FFI bindings, compromising memory safety, complicating static linking and cross-compilation, and inviting segmentation faults on unbounded recursion.
- Smirk smirks at both traps: by executing bytecode over an explicit, heap-allocated Pike-style backtracking state machine under a strict Fuel / Step Budget, Smirk guarantees that execution never locks a runtime thread, never overflows the C call stack, and can pause, yield an intact continuation, and resume on demand.
Core Motivation & Architecture
The Problem with C FFI and Unbounded Backtracking
- Foreign Function Interface (FFI) Vulnerabilities: Traditional Haskell bindings to C PCRE libraries introduce native build dependencies, cross-compilation obstacles, and potential memory leaks across C string marshalling boundaries.
- Catastrophic Backtracking (ReDoS): When processing untrusted user input (e.g. web forms, query filters, search strings), standard regex engines can enter exponential search loops that freeze application threads.
- Container Inflexibility: Most regex libraries force data into strict
ByteString or Text, forcing expensive allocations when scanning [Char], finger-tree Seq Char, or Vector Char.
- Effect Monolithic Coupling: Extensible effect users often find regex libraries tightly coupled to a single effect framework, pulling in heavy dependencies.
The Smirk Solution
smirk resolves these challenges through five architectural pillars:
- 100% Pure Haskell Pike Virtual Machine: Patterns are compiled into linear bytecode vectors (
Opcode), evaluated by a step-stepped virtual machine with an explicit heap-allocated backtracking stack.
- Fuel-Bounded Execution & ReDoS Defense: Every instruction execution consumes 1 unit of
Fuel (StepBudget !Int). If the budget is exhausted, the engine yields OutOfFuel along with a MatchContinuation.
- Pausable & Resumable Continuations: Match evaluations can be paused, inspected, serialized, and resumed via
resumeMatch with an replenished fuel budget.
- Sequence Polymorphism: Through the
RegexTarget abstraction, matching operates zero-copy across Text, ByteString, [Char], Seq Char, and Vector Char.
- Decoupled 3-Package Architecture: The core engine is completely independent of effect libraries; dedicated packages provide zero-overhead bindings for
effectful and polysemy.
Multi-Package Catalog
The repository is structured into four independent, modular Cabal packages:
| Package |
Directory |
Description |
Hackage Status |
smirk |
./ |
Core engine: Megaparsec PCRE parser, AST, bytecode compiler, Pike VM, sequence polymorphism, glob engine, and QuasiQuoters (`[re |
... |
smirk-effectful |
../smirk-effectful/ |
First-class algebraic effect (RegexEngine) for the effectful ecosystem, featuring pure and fuel-bounded interpreters. Depends only on smirk and effectful. |
Ready (0.1.0.0) |
smirk-polysemy |
../smirk-polysemy/ |
Extensible effect (RegexEngine) for the polysemy ecosystem, featuring pure and fuel-bounded interpreters. Depends only on smirk and polysemy. |
Ready (0.1.0.0) |
smirk-penelope |
../smirk-penelope/ |
Algebraic effect façade (RegexEngine) for the penelope effect system, featuring zero-cost vector-dispatched interpreters over flat Polytropos row vectors. |
Ready (0.1.0.0) |
Key Subsystems & Worked Examples
1. Basic PCRE Matching & Capture Groups
{-# LANGUAGE OverloadedStrings #-}
import Smirk
main :: IO ()
main = do
case compileRegex "(?<protocol>https?)://(?<domain>[a-z.]+)" of
Left err -> putStrLn ("Compile error: " ++ show err)
Right reg -> do
case execMatch reg ("Visit https://haskell.org today!" :: Text) of
Nothing -> putStrLn "No match found"
Just res -> do
print (matchedSlice res) -- "https://haskell.org"
print (matchOffset res, matchLength res) -- (6, 19)
print (namedGroups res) -- fromList [("domain","haskell.org"),("protocol","https")]
2. Compile-Time QuasiQuoters ([re|...|] & [ed|...|])
QuasiQuoters validate regex syntax and stream editor templates at compile time:
{-# LANGUAGE QuasiQuotes #-}
import Smirk
-- 1. Compile-time verified pattern:
let pattern = [re|\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b|]
-- 2. Stream editor find-and-replace:
let editor = [ed|foo([0-9]+)///bar${1}|]
let replaced = applyEditor editor "foo42 and foo99"
-- replaced == "bar42 and bar99"
3. Step-Bounded Execution & ReDoS Defense
Defend against pathological catastrophic backtracking patterns by enforcing a step budget:
import Smirk
let pathological = compileRegex "(a+)+$"
let target = "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" :: Text
case pathological of
Right reg ->
-- Restrict execution to at most 10,000 VM instruction steps:
case execMatchWithFuel reg target (StepBudget 10000) of
Matched _ -> putStrLn "Matched"
NoMatch -> putStrLn "Did not match"
OutOfFuel cont -> do
putStrLn "ReDoS detected! Step budget exceeded without locking CPU."
-- Optionally resume later with additional fuel:
case resumeMatch cont (StepBudget 5000) of
OutOfFuel _ -> putStrLn "Still out of fuel; safely aborting."
_ -> putStrLn "Finished."
4. Sequence Polymorphism across Haskell Containers
Match uniformly across different sequence representations without converting to Text:
import qualified Data.ByteString.Char8 as BS
import qualified Data.Sequence as Seq
import qualified Data.Vector as V
import Smirk
let reg = [re|[0-9]+|]
-- Strict ByteString:
testMatch reg (BS.pack "port 8080") -- True
-- Standard String ([Char]):
testMatch reg "port 8080" -- True
-- Data.Sequence.Seq Char:
testMatch reg (Seq.fromList "port 8080") -- True
-- Data.Vector.Vector Char:
testMatch reg (V.fromList "port 8080") -- True
5. Effectful Integration (smirk-effectful)
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}
import Effectful
import Smirk (re)
import Smirk.Effect.Effectful
lookupPort :: (RegexEngine :> es) => Text -> Eff es (Maybe Text)
lookupPort input = do
mMatch <- matchRegex [re|port:([0-9]+)|] input
pure (fmap matchedSlice mMatch)
6. Polysemy Integration (smirk-polysemy)
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}
import Polysemy
import Smirk (re)
import Smirk.Effect.Polysemy
lookupPort :: (Member RegexEngine r) => Text -> Sem r (Maybe Text)
lookupPort input = do
mMatch <- matchRegex [re|port:([0-9]+)|] input
pure (fmap matchedSlice mMatch)
7. Native Filesystem Globbing ([glob|...|])
{-# LANGUAGE QuasiQuotes #-}
import Smirk
let g = [glob|src/**/*.{hs,lhs}|]
matchGlob g "src/Smirk/Core/AST.hs" -- True
matchGlob g "test/Spec.hs" -- False
Documentation Corpus & Reading Order
The technical documentation corpus is structured for progressive disclosure:
docs/INDEX.md — Comprehensive corpus inventory, audience reading paths, and package catalog.
docs/TUTORIAL.md — Detailed hands-on tutorial with worked examples across all 7 subsystems.
docs/NEWBIE_GUIDE.md — Fast symbol navigation table and 15-minute quickstart.
docs/MAINTAINER_GUIDE.md — Architectural invariants (I1–I8), verification gates, and triage runbooks.
docs/notes/SMIRK_DESIGN.md — Canonical design monograph detailing decisions D1–D10.
docs/notes/DOC_STRATEGY.md — Documentation strategy, precedence rule, and lifecycle.
docs/notes/COMMIT_ATTRIBUTION.md — Linux kernel RFC 2822 trailer format standards.
AGENTS.md — Advisory operational rules and navigation funnels for AI coding assistants.
Physical Verification Discipline
Before any commit or release, all 5 physical verification gates must pass cleanly:
./bin/smirk-verify
- Gate 1: HLint Static Analysis: Zero hints allowed across all source packages (
.hlint.yaml).
- Gate 2: Full Cabal Test Suite: All test suites pass across all packages (
cabal test all).
- Gate 3: Tagref Cross-Reference Gate: Zero dangling references and zero duplicate tags (
bin/smirk-tagref).
- Gate 4: Markdown Link Integrity Gate: Every relative link and heading anchor resolves (
bin/smirk-linkcheck).
- Gate 5: CLOC Comment Density Gate: Comment density satisfies documentation thresholds (
bin/smirk-cloc).
License & Attribution
- Licensed under the Apache License 2.0 (
LICENSE).
- Copyright (c) 2026 Nadia Yvette Chambers.
- AI assistant contributions follow the RFC 2822 trailer format (
Assisted-by:). See docs/notes/COMMIT_ATTRIBUTION.md.