Skip to main content

Crate irgx

Crate irgx 

Source
Expand description

Rust bindings for the irregex regex engine.

The API is the regex crate’s shape - Regex::new, Regex::is_match, Regex::find, Regex::find_iter, Regex::captures, Regex::split, Regex::replace_all - because that is the API a Rust programmer already knows. What is behind it is a Zig engine linked into your process, reached through a small C ABI.

let re = irgx::Regex::new(r"(\w+)@(\w+)")?;
let caps = re.captures("mail bob@host now").unwrap();
assert_eq!(&caps[1], "bob");
assert_eq!(caps.get(2).unwrap().as_str(), "host");

§Compiling, and the two ways a pattern is refused

There are two grammars here, so a refused pattern splits into two facts with two different repairs, and they are two variants rather than one string.

Error::NeedsPcre means the pattern is fine and only the linear grammar cannot express it - lookaround, a backreference, a flag letter it does not have ((?x), (?U), (?R)). A leading (?i) is not in that list: it is read as the flag it asks for, as regex reads it, and compiles. The same pattern under RegexBuilder::pcre compiles, so the retry is a match arm:

use irgx::{Error, Regex, RegexBuilder};

fn compile(pattern: &str) -> Result<Regex, Error> {
    match Regex::new(pattern) {
        Err(Error::NeedsPcre { .. }) => RegexBuilder::new(pattern).pcre(true).build(),
        other => other,
    }
}

assert_eq!(compile(r"(?<=\$)\d+")?.find("cost $42").unwrap().as_str(), "42");

It is not retried for you because the PCRE2 arm is not linear in the length of the text, and a program compiling somebody else’s patterns may want to decline rather than accept that.

Error::Syntax means the pattern is malformed, and carries the byte offset the engine stopped at. pcre will not rescue it, so retrying only fails twice. The offset is always a real index into the pattern - never past the end, never mid-codepoint - so &pattern[..at] is what the engine got through:

let Err(Error::Syntax { at, .. }) = Regex::new("(unclosed") else { unreachable!() };
assert_eq!(at, 9);

§Threads

Regex is Send + Sync, so the idiom works:

use std::sync::LazyLock;
use irgx::Regex;

static WORD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\w+").unwrap());

let total: usize = std::thread::scope(|scope| {
    let handles: Vec<_> = ["one two", "three", "four five six"]
        .map(|text| scope.spawn(move || WORD.find_iter(text).count()))
        .into_iter()
        .collect();
    handles.into_iter().map(|h| h.join().unwrap()).sum()
});
assert_eq!(total, 6);

The C handle underneath is single-threaded: it owns the scratch its searches run in. So a Regex owns a pool of handles and leases one per search. The cost is one extra compile the first time a given level of concurrency is reached, an uncontended mutex per search, and handles freed when the Regex drops. Nothing is thread-bound and nothing leaks into a thread that outlives the pattern.

§Offsets are bytes

Match::start and Match::end are byte offsets into the &str you searched, which is the engine’s own coordinate system - &text[m.range()] is the matched text, no translation involved. A pattern compiled with RegexBuilder::unicode off matches bytes, so it can report a boundary inside a codepoint; that is Error::NotCharBoundary rather than a panic in your slicing code.

§How this differs from the regex crate

  • Regex::find_iter is eager, and therefore knows its length and runs backwards. The sequence itself is the regex crate’s, empty matches included — a* over "abc" is (0,1), (2,2), (3,3) in both — and the differential in tests/sequence.rs holds it there over a corpus of nullable patterns.
  • Lookaround and backreferences exist, behind RegexBuilder::pcre. The default engine is linear in the length of the text; the PCRE2 arm is not. A pattern that needs the other arm is Error::NeedsPcre, not a syntax error, so regex’s single Error::Syntax(String) becomes two variants here.
  • RegexBuilder::fixed, RegexBuilder::word and RegexBuilder::smart_case are first-class flags, not things you build by rewriting the pattern.
  • Faults are possible after compiling. The regex-shaped verbs panic on one; each has a try_ sibling that returns Error.
  • Munch has no regex-crate counterpart at all. It answers the question a tokenizer asks and a search cannot: starting at exactly this offset, over these patterns, which reaches furthest? Maximal munch, with the permitted set narrowed per call, which is what makes a state-directed lexer possible without stepping the automaton by hand.

§Linking

The crate carries a prebuilt static archive per supported target, so the usual build needs no Zig toolchain. IRGX_LIB_DIR points the build at a library you built yourself instead. A target with no vendored archive falls back to building the engine from source, and fails at build time with a sentence if it cannot.

Modules§

codex
Count, locate and restore a text the index does not store. A self-index: it answers about a text it does not store, and can hand the text back.
contract
Shared contract mirrors — engine/analytic/kinship constants and row tables. Runtime mirror of the substrate contracts — irregex/contract/engine.toml, irregex/contract/analytic.toml, and the kinship package’s contract/kinship.toml — plus the result records both planes report.
corpus
Searching a TREE rather than a buffer you already hold: the tree, walk and sieve planes, and the corpus that warms them. Searching a TREE, not a buffer you already hold.
lines
The line grid: rows, bands, and the off-by-one that lives here instead of in your host. The line grid: rows, bands, and the off-by-one that lives here instead of in your host.
needles
Many literals, one pass, with attribution. Many literals, one pass, with attribution.
promise
What a pattern PROMISES about every byte sequence it can match. What a pattern PROMISES about every byte sequence it can match.
request
The unified SearchRequest → match stream for the exact plane. The unified SearchRequest — one search expressed once, runnable on any face.
runtime
Transports, the analytic ladder, and the substrate runtime::Error.
unicode
The Unicode tables this engine folds and classifies with. The Unicode tables this engine folds and classifies with.

Structs§

CaptureMatches
The capture groups of every match in one text.
Captures
The capture groups of one match.
Match
One match: a byte range in the text that was searched.
Matches
Every match in one text, in the sequence the regex crate reports.
Munch
Many patterns, matched anchored at one offset, longest wins.
MunchBuilder
Compile a Munch with the flags spelled out.
NoExpand
A replacement string with no $ expansion: every byte of it is literal.
Refusal
One pattern a Munch could not take.
Regex
A compiled pattern.
RegexBuilder
Compile a pattern with the flags spelled out.
RegexSet
Many patterns, matched against one text in a single pass.
RegexSetBuilder
Compile a RegexSet with the flags spelled out.
SetMatches
Which patterns of a RegexSet matched one text.
Split
The pieces of a text between its matches.
Status
One raw status code from the C ABI, with the library’s own sentence for it.
Token
What a scan found: how far it reached, and which patterns got there.

Enums§

Answer
An answer, or the tier declining to give one.
Error
Everything that can go wrong between a pattern and an answer.
Pick
Which reading of an offset a scan takes.
Why
Why one pattern could not become an anchored automaton.

Constants§

ABI_VERSION
The C-ABI version this crate speaks. The linked library must report the same number or every Regex::new fails with Error::Abi.

Traits§

Replacer
What a replacement is made of.

Functions§

engine_version
The linked engine’s semantic version, e.g. "1.0.0".
pcre2_version
The vendored PCRE2 version the RegexBuilder::pcre arm runs on.