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_iteris eager, and therefore knows its length and runs backwards. The sequence itself is theregexcrate’s, empty matches included —a*over"abc"is(0,1), (2,2), (3,3)in both — and the differential intests/sequence.rsholds 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 isError::NeedsPcre, not a syntax error, soregex’s singleError::Syntax(String)becomes two variants here. RegexBuilder::fixed,RegexBuilder::wordandRegexBuilder::smart_caseare 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 atry_sibling that returnsError. Munchhas noregex-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’scontract/kinship.toml— plus the result records both planes report. - corpus
- Searching a TREE rather than a buffer you already hold: the
tree,walkandsieveplanes, 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 unifiedSearchRequest— 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§
- Capture
Matches - 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
regexcrate reports. - Munch
- Many patterns, matched anchored at one offset, longest wins.
- Munch
Builder - Compile a
Munchwith the flags spelled out. - NoExpand
- A replacement string with no
$expansion: every byte of it is literal. - Refusal
- One pattern a
Munchcould not take. - Regex
- A compiled pattern.
- Regex
Builder - Compile a pattern with the flags spelled out.
- Regex
Set - Many patterns, matched against one text in a single pass.
- Regex
SetBuilder - Compile a
RegexSetwith the flags spelled out. - SetMatches
- Which patterns of a
RegexSetmatched 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::newfails withError::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::pcrearm runs on.