Files
retoorandClaude Sonnet 5 e4fc067aa6 Eliminate the Pike VM's per-thread allocation, add a real BMH literal
prefilter, research and reject a lazy DFA cache

Researched two production Pike-VM-family engines directly (RE2's
nfa.cc, rust-lang/regex's regex-automata), fetched and read, not
recalled, specifically for what explains the two weaknesses the last
benchmark found (a*b's nullable loop, dense finditer): both avoid
per-thread malloc/free, RE2 via a Thread free list, regex-automata via
a flat SlotTable indexed directly by NFA state. Also researched
Hyperscan's Teddy (SIMD literal matching) and set it aside: it needs
platform-specific intrinsics, in tension with this project's
single-file, portable-C, simplicity-over-performance priority: a
portable Boyer-Moore-Horspool skip was judged proportionate where
Teddy was not.

Rewrote the Pike VM's memory model accordingly. PikeThread's owned
int64_t* is gone; a committed thread's capture row now lives at a
fixed offset in PikeList.table, indexed directly by instruction PC
(Cox's one-thread-per-PC invariant already made this index unique, so
no allocation is needed to store or discard one). Transient rows
needed mid-closure, before the eventual terminal PC is known, come
from ScratchPool, a fixed block with an explicit free list; a plain
bump/decrement counter was tried first and proven incorrect by hand
before being written into the file (an OP_SPLIT's second branch's row
can outlive several non-branching pushes that reuse an *earlier* row
without reallocating it, which only a real free list handles safely
regardless of release order). The closure stack is pre-sized once
instead of grown by realloc on demand. All three, plus a reusable
best-match buffer and the prefilter below, bundle into one PikeEngine,
built once per top level Pattern_/re_ call and reused across every
match found within it, never cached on PatternImpl itself (that would
make concurrent Pattern_search calls on the same compiled Pattern from
different threads race on shared state, breaking the existing
no-synchronization-needed guarantee for a Pattern nothing mutates).

Extended the literal prefilter from a single leading character to the
full mandatory literal prefix, with a real Boyer-Moore-Horspool
bad-character skip table for BINARY/ASCII mode (UTF8 keeps a
without-skip fallback: a byte-indexed table cannot cover code points
past 0x10FFFF). The skip-ahead only ever applies to where a new
unanchored start is injected, never to advancing sp itself while a
thread from an earlier start position is still alive.

Researched and did not build a lazy DFA state cache (memoizing a
live-instruction-set-plus-byte transition). Not an omission: RE2's own
lazy DFA cannot track submatch boundaries, the same structural reason
applies here, since every call wants at least group 0's span, and a
cached transition only answers whether a match is possible, not which
path was taken; using one would need a two-phase architecture deserving
its own research-and-plan pass. Checked empirically too: a*b, the case
such a cache would help most, has at most two live instructions at any
position for its whole run, so there is no repeated state worth
caching in the first place.

Verified: full 3,252-case suite (four clean runs), a whitebox
dual-engine cross-check extended to also cover finditer/split/BINARY
mode (30,000 + 3,334 + 2,500 comparisons, zero mismatches), a targeted
suite for the new prefilter machinery including the classic
Boyer-Moore-Horspool overlapping-suffix correctness trap (12/12), two
clean AddressSanitizer/UndefinedBehaviorSanitizer passes on each (a
third run of each hit the same pre-existing sandbox flake already
documented, confirmed unrelated by retrying clean).

Measured: literal search went from 0.64x of the backtracking engine's
time (already ahead) to 0.03x (~33x faster), and against POSIX
<regex.h> from roughly 11x slower to roughly 2x *faster* than glibc
outright; a*b (no prefilter benefit at all) improved from 3.45x slower
than backtracking to 1.97x, from the allocation fix alone; dense
finditer over [0-9]+/\w+ improved from 3.2x/5.6x slower to 1.7x/2.8x.
Full tables and citations in concept.md 7.10; README.md, docs/API.md,
USAGE.md, and bench_vs_posix.c's own printed summary updated
throughout with the corrected numbers and the full history, not just
the final ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY
2026-09-14 18:49:21 +00:00

32 KiB

regexx API Reference

This document records, exhaustively, the complete public surface of regexx.h/regexx.c: every type, every flag, every function, its exact return-value convention, its memory ownership rule, and its relationship to the corresponding Python re name. concept.md records the design rationale; README.md is the short entry point (build, usage, implementation status at a glance). This document is the complete reference the other two point to when a precise answer is needed.

Every fact below was checked against the current source (regexx.c, regexx.h) and, where a Python behavior is cited, against a real CPython 3 interpreter, not against memory or documentation alone.

1. Types

1.1 Pattern (re.Pattern)

struct Pattern {
    const char *pattern;     /* re.Pattern.pattern */
    int         flags;       /* re.Pattern.flags */
    int         groups;      /* re.Pattern.groups */
    void       *groupindex;  /* re.Pattern.groupindex, opaque, see 3.11 */
    void       *program;     /* private: compiled bytecode */
};
  • pattern: the exact source text passed to re_compile, NUL-terminated, owned by the Pattern (freed by Pattern_free). Read-only for callers.
  • flags: the exact flags value passed to re_compile, including the encoding-mode bits (BINARY/ASCII/UTF8) and any leading global inline flags folded in during parsing (Section 2.6).
  • groups: the number of capturing groups, matching Python's re.Pattern.groups exactly (group 0, the whole match, is not counted).
  • groupindex: opaque; see Pattern_groupindex_lookup/_count/_at (3.11) for the supported access to it.
  • program: private, never dereference directly.

A Pattern is created only by re_compile (directly, or indirectly through the module level cache in re_match/re_search/etc.) and is freed only by Pattern_free.

1.2 Match (re.Match)

struct Match {
    Pattern    *re;         /* re.Match.re */
    Input      *string;     /* re.Match.string */
    int64_t     pos, endpos; /* re.Match.pos, re.Match.endpos */
    int         lastindex;   /* re.Match.lastindex */
    const char *lastgroup;   /* re.Match.lastgroup */
    void       *slots;       /* private */
};
  • re: the Pattern that produced this match. Borrowed reference; do not free it through this pointer, and do not call Pattern_free on it while any Match from it is still in use.
  • string: the Input the match was found in. Borrowed reference, same lifetime rule as re.
  • pos, endpos: the effective search bounds used to produce this match (the pos/endpos arguments to whichever Pattern_ function created it, clamped to [0, length]), in the same unit as Match_start/Match_end (code point index in UTF8 mode, byte offset otherwise). Matches re.Match.pos/.endpos exactly.
  • lastindex: the highest-numbered capturing group that participated in the match, or -1 if none did. Deviation from CPython: Python's lastindex is "the index of the last group to match", which for a pattern with re-entrant alternation can differ from "the highest-numbered group that participated"; this build uses the latter, simpler rule. They coincide for every straightforward pattern (any pattern without a capturing group inside a repeated alternative that can also match via a different, lower-numbered branch later in the same attempt).
  • lastgroup: the name of that group, or NULL if it is unnamed or lastindex is -1. Borrowed pointer into the Pattern's group name table; valid as long as the Pattern is.
  • slots: private.

A Match is filled in by one of the Pattern_/re_ matching functions (never allocated separately by the caller: pass the address of a stack or heap Match struct as out, zero-initialize it first). Its private slots are freed by Match_free, which does not free the Match struct itself (the caller owns that memory, stack or heap).

1.3 Input (no Python counterpart)

Opaque. Python's re never streams; it always operates on an in-memory str/bytes object already held by the caller. Input is the type this design adds so a C caller has an explicit thing to construct from a buffer or a file (concept.md 9.0/9.2).

Input *Input_from_buffer(const uint8_t *buf, size_t len);
Input *Input_from_file(const char *path, PatternError *err);
void   Input_free(Input *in);
  • Input_from_buffer: wraps an existing buffer. Does not copy it and does not take ownership. The buffer must outlive the Input and every Match produced from it (Match_group returns pointers directly into it, Section 3.9). Freeing an Input_from_buffer Input never frees the underlying buffer; the caller is responsible for that.
  • Input_from_file: for a regular file, mmaps it read-only (MAP_PRIVATE) instead of copying it into the heap, so the pages are backed by the file and the kernel can reclaim them under memory pressure (README.md "Memory footprint"). For a non-seekable source (pipe, FIFO, process substitution, /dev/stdin) or when mmap itself fails, it falls back to reading incrementally into a growable, owned heap buffer. Either way, the entire input is addressable before any matching happens (README.md "Implementation status"); only the second path actually copies it. On failure (cannot open, cannot allocate) returns NULL and, if err is non-NULL, fills it with strerror(errno) as msg.
  • Input_free: frees the Input struct itself always, and additionally releases the underlying buffer unless it came from Input_from_buffer: munmaps it if it was mapped, or frees it if it was read into an owned heap buffer (both cases of Input_from_file).

1.4 PatternError (re.error / re.PatternError)

struct PatternError {
    const char *msg;      /* re.error.msg */
    const char *pattern;  /* re.error.pattern */
    int64_t     pos;      /* re.error.pos */
    int64_t     lineno;   /* re.error.lineno */
    int64_t     colno;    /* re.error.colno */
};
void PatternError_free(PatternError *err);

Named after the alias CPython itself introduced for re.error (re.PatternError, concept.md 9.0). Fields match CPython's re.error attributes (added in CPython 3.5) exactly: msg is the human-readable message, pattern is the offending pattern text (or NULL when the error is not about pattern syntax, for example Input_from_file failing to open a file), pos is the byte offset into pattern the error was detected at, lineno/colno are computed from pos the same way CPython computes them (1-based, counting \n bytes in pattern up to pos).

Ownership: msg and pattern are heap allocated (strdup) by whichever call filled the struct in. Zero-initialize a PatternError before passing its address in, and call PatternError_free on it once done reading it, whether or not the call that filled it in succeeded (a NULL err argument to any function is always safe to pass and simply skips error reporting). PatternError_free is safe to call on an all-zero or already-freed PatternError.

Every function that can fail accepts an optional PatternError *err (pass NULL to ignore); re_compile and Input_from_file are the two that actually produce one today. re_match/re_search/etc. (the module level convenience functions, 3.12) do not expose a PatternError parameter at all, matching the fact that CPython's own re.match/re.search/etc. do not return one either (a syntax error there raises, which has no C equivalent; here it instead causes the call to return -1 with no further diagnostic, which is why Pattern-based usage, not the module level convenience functions, is recommended whenever a compile error needs to be reported, concept.md/README "recommended entry point" note).

1.5 MatchIterCb / MatchSubCb

typedef void (*MatchIterCb)(void *ctx, const Match *m);
typedef void (*MatchSubCb)(void *ctx, const Match *m, char **out, size_t *outlen);

MatchIterCb is invoked once per result by Pattern_finditer, Pattern_findall (an alias of finditer in this build, 3.7), and Pattern_split (3.8, with a different per-call meaning, documented there). The Match passed in is only valid for the duration of the call; it is freed immediately after the callback returns, so do not retain the pointer.

MatchSubCb is the C shape of Python's callable repl argument to re.sub/re.subn: invoked once per match, expected to malloc a replacement buffer, write its address into *out and its length into *outlen. The callback's *out becomes owned by Pattern_sub/Pattern_subn, which frees it after copying its content into the final result buffer.

2. Flags

Passed as flags to re_compile or any re_ module level function, combined with bitwise |, using the exact spelling CPython uses (concept.md 9.0).

Flag Value Effect Status
IGNORECASE 0x0001 Case-insensitive literal, class, and backreference comparison. Class ranges are matched under both a character's original case and its swapped case (README "Known deviations": an approximation, not full Unicode case folding). Implemented
MULTILINE 0x0002 ^/$ also match at the start/end of each line, not only the start/end of the string. Implemented
DOTALL 0x0004 . matches \n too. Implemented
VERBOSE 0x0008 Unescaped whitespace and #-to-end-of-line comments outside character classes are stripped from the pattern before parsing. Implemented
ASCII 0x0010 Two roles: (a) with neither BINARY nor UTF8 also given, selects the ASCII encoding mode (Section 2, below); (b) in UTF8 mode, forces \d/\w/\s and IGNORECASE swapcase to their ASCII-only definitions instead of consulting wctype.h. Implemented
UNICODE 0x0020 Accepted for source compatibility with Python. No effect: UTF8 mode already behaves as CPython's default (Unicode) str matching, so there is no separate "unicode" flag needed the way ASCII needs one to opt out. Accepted, no-op
LOCALE 0x0040 Accepted for source compatibility with Python. No observable effect: this build commits to the "C" locale only (concept.md 13.4), and under the "C" locale, \w/\b/\B classify only the ASCII letters and digits regardless of this flag, matching both the C standard's own guarantee for isalnum() under "C" and a real CPython interpreter's re.LOCALE explicitly set to "C" (verified directly; an earlier revision of this build instead treated bytes 0x80-0xFF as word characters under this flag, based on an unverified assumption about "C" locale behavior that turned out to be false, caught by the combinatorial test expansion, corrected in regexx.c's cls_is_word). Accepted, no-op
DEBUG 0x0080 Accepted for source compatibility with Python. No effect in this build: nothing is printed and matching is unaffected. Accepted, no-op
BINARY 0x0100 Encoding mode: raw bytes, 0-255 all valid, no decoding, embedded NUL is an ordinary byte. Implemented
UTF8 0x0200 Encoding mode: input is decoded as UTF-8 into code points before matching; Match_start/Match_end report code point indices (Match_start_byte/Match_end_byte report byte offsets, Section 3.9). Implemented

Exactly one encoding mode is active for any compiled Pattern: BINARY and UTF8 are checked first, in that order, and if neither is present the mode is ASCII regardless of whether the ASCII flag bit itself was set (so re_compile(p, n, 0, &err) and re_compile(p, n, ASCII, &err) compile to the same encoding mode; only combining ASCII with UTF8 changes anything, per row (b) above).

3. Functions

Return value convention used throughout, unless noted otherwise for a specific function: 1 success/matched, 0 no match (not an error), -1 an error occurred (invalid UTF-8 in the subject for a UTF8-mode Pattern, or the backtracking depth limit was reached, README.md "Implementation status"; no further detail is available through the return value itself in this build, only through the fact that it is negative rather than 0).

3.1 re_compile

Pattern *re_compile(const char *pattern, size_t len, int flags, PatternError *err);

Compiles pattern (len bytes, need not be NUL-terminated) under flags into a fresh, independent Pattern, matching re.compile. Returns NULL and fills err (if non-NULL) on a syntax error, an unsupported construct (Section 5 of this document lists all of them), or a lookbehind that is not fixed-width. Never consults or populates the module level cache (3.12); always allocates a new Pattern, freed only by Pattern_free.

int Pattern_match(Pattern *self, Input *string, int64_t pos, int64_t endpos, Match *out);
int Pattern_fullmatch(Pattern *self, Input *string, int64_t pos, int64_t endpos, Match *out);
int Pattern_search(Pattern *self, Input *string, int64_t pos, int64_t endpos, Match *out);

Mirror re.Pattern.match/.fullmatch/.search exactly, including the pos/endpos parameters (pass 0 and -1 for CPython's own defaults, "search the whole string"). pos/endpos are in the pattern's native unit (code point index for UTF8 mode, byte offset otherwise, Section 1.2); a negative endpos means "to the end". out must point to a zero-initialized Match (or one already released with Match_free); on a 0 or -1 return it is left untouched.

  • match: anchored at pos, need not reach endpos.
  • fullmatch: anchored at pos, must also reach exactly endpos.
  • search: tries every start position from pos to endpos inclusive, left to right, and reports the first that admits any match (ordinary backtracking priority decides which match that is at that position, concept.md 2.5).

Performance: which of two engines runs a given Pattern is decided once, at re_compile time, and is never a caller's choice (README.md "Implementation status", concept.md 7.2/7.7). A pattern with no backreference, lookahead, lookbehind, or atomic group (a possessive quantifier already desugars to the last of these) runs on the Pike VM, a Thompson-NFA simulation with no backtracking at all: every operation in this section is genuinely linear in input length on that engine, including search against an adversarial pattern shape like (a+)+b that would otherwise invite catastrophic backtracking, with no atomic group needed (README.md "The Pike VM"). Everything else (a backreference anywhere, or a lookaround/atomic construct) runs on the recursive backtracking engine instead: match/fullmatch there do one anchored attempt at time proportional to that attempt; search tries each candidate start position as a separate attempt but, unlike a naive backtracking search, does not redo the same work at every one, since run_memo caches every proven failure at the (instruction, position) level for a backreference-free pattern on this engine, and OP_REPEAT1 additionally uses precomputed skip-ahead tables, together making search linear rather than quadratic for a pattern built from simple repeated atoms; a repeat over a compound body only gets the failure-memoization, and a pattern with a backreference disables memoization entirely (unsound there, Section 5) and can still be worst-case exponential, exactly as in CPython. The Pike VM has no per-thread allocation at all (a flat table indexed directly by instruction, not a malloc/free per split, concept.md 7.10) and a real Boyer-Moore-Horspool literal prefilter for a pattern beginning with a mandatory literal string (BINARY/ASCII mode; UTF8 mode gets a without-skip fallback, still real, since a byte-indexed skip table cannot cover code points past 0x10FFFF); a pattern beginning with a literal now typically outperforms the backtracking engine outright, and often POSIX <regex.h> too (README.md "Benchmarks"). A pattern that cannot be prefiltered at all (a nullable leading loop, or a leading character class with no fixed literal to extract) still measures somewhat slower in absolute terms than the backtracking engine would on the same pattern, a real, narrower-than-before, and deliberately not further optimized trade-off: concept.md 7.10 also records researching a lazy DFA state cache for exactly this remaining gap and not building it, both because it cannot track captures without a separate two-phase architecture and because the pattern shape it would help most (a*b) was checked directly and found to have no repeated state worth caching in the first place. See README.md "Implementation status" and "The Pike VM" for the measured numbers on both engines and citations to the published techniques each uses.

Memory: the tables above cost real, measured memory, not just time complexity: search/finditer/split/sub on a pattern using OP_REPEAT1 peak at roughly 8.4x the input length (measured with Valgrind/Massif; match/fullmatch do not allocate these tables at all and use proportionally less). README.md "Memory footprint" has the full measured breakdown, including a fix that was tried, measured, and deliberately reverted because it traded a fast allocation failure for an effectively-unbounded hang, which is a worse failure mode, not a better one.

3.5-3.6 Pattern_finditer / Pattern_findall

int Pattern_finditer(Pattern *self, Input *string, int64_t pos, int64_t endpos, MatchIterCb cb, void *ctx);
int Pattern_findall(Pattern *self, Input *string, int64_t pos, int64_t endpos, MatchIterCb cb, void *ctx);

Pattern_findall is defined as a call to Pattern_finditer with the same arguments; both invoke cb(ctx, m) once per non-overlapping match, left to right, applying CPython's own empty-match rule exactly, including the part of it CPython does not document (concept.md 3, regexx.c's Pattern_finditer comment): if the match found at a given start position is empty, a second, non-empty match is additionally searched for and reported at that same start before the scan moves on, so \d*? against "123abc456" yields 16 matches, not 9, matching a real CPython interpreter exactly (verified by generating this case's expectation from one, tests/cases.py). Returns the number of matches found, or -1 on error. Shares one memoization table and one set of OP_REPEAT1 precomputed tables across the whole scan (built once, not once per match), so it inherits search's performance characteristics in Section 3.2-3.4 exactly, not a worse case from repeating the scan. This build does not collapse a no-groups match down to "just the matched string" or a multi-group match to a Python tuple the way re.findall does at the Python level; the callback always receives a full Match, from which the caller reads whatever it needs via Match_group. This is a deliberate simplification: findall's string/tuple collapsing is a Python-object-model convenience with no C equivalent to collapse into, so this build gives the caller the same, uniform Match-based access finditer does, and the two functions exist separately only for name-for-name parity with re.findall/re.finditer.

3.7 Pattern_split

int Pattern_split(Pattern *self, Input *string, int maxsplit, MatchIterCb cb, void *ctx);

cb is invoked once per element of the list re.split() would return, in order: this is the entire contract, and it is unambiguous by construction (earlier drafts of this function called cb twice per match with the caller left to infer which call meant what; that design was replaced before release specifically because it was ambiguous). Each element's text is read via Match_group(m, NULL, 0, &out, &outlen); a 0 return from that call means this element is Python's None (an unparticipated capturing group between two matches), matching how an unparticipated group reports on any other Match. maxsplit matches re.split's parameter (0 means unlimited). Returns the number of matches that were split on (not the number of list elements), or -1 on error. Built on the same scan as Pattern_finditer and follows the same empty-match rule (Section 3.5-3.6), which is why a pattern that can match empty (\d*?, x*, and so on) produces the long runs of empty-string list elements a real CPython re.split() does, not a shorter list that only advances once per empty match. Shares one memoization table and one set of OP_REPEAT1 precomputed tables across the whole scan; see Section 3.2-3.4's performance note.

3.8-3.9 Pattern_sub / Pattern_subn

int Pattern_sub(Pattern *self, Input *string, const char *repl, MatchSubCb cb, void *ctx, int count, char **out, size_t *outlen);
int Pattern_subn(Pattern *self, Input *string, const char *repl, MatchSubCb cb, void *ctx, int count, char **out, size_t *outlen, int *n);

Mirror re.Pattern.sub/.subn. Exactly one of repl (a template string) or cb (a callback) must be non-NULL; passing both or neither is a caller error with unspecified behavior. This two-parameter shape is the direct C consequence of Python's single repl argument being polymorphic (string or callable) in a way C's static type system cannot express in one slot (concept.md 9.4, 9.0).

repl template syntax: \g<name>, \g<N>, \N (one or two digits), \n, \t, \\, and any other \X as the literal character X, matching concept.md Section 3.

count matches re.sub's count parameter (0 means unlimited; a positive count stops substituting after that many matches, leaving the rest of the subject, including any further matches within it, untouched, exactly as CPython leaves it).

Pattern_sub and Pattern_subn differ only in whether the number of substitutions actually made is reported back through n (mirroring re.sub returning just the string versus re.subn returning (string, count)); Pattern_sub is implemented as a call to Pattern_subn with a throwaway n. Both are implemented on top of Pattern_finditer and so share its performance characteristics, Section 3.2-3.4's performance note.

*out is a freshly malloc'd, NUL-terminated buffer of length *outlen; the caller must free it. On -1 (error), *out/*outlen are left untouched.

3.10 Pattern_free

void Pattern_free(Pattern *self);

Frees a Pattern and everything it owns (the compiled program, the retained parse tree, groupindex, the copy of the pattern text). Do not call this while any Match produced from this Pattern is still in use (Match.re/Match.lastgroup borrow from it, Section 1.2); free every such Match with Match_free first, or simply free them in the reverse order they were created, which is always safe.

3.11 Pattern_groupindex_lookup / _count / _at

int Pattern_groupindex_lookup(Pattern *self, const char *name);
int Pattern_groupindex_count(Pattern *self);
int Pattern_groupindex_at(Pattern *self, int i, const char **name);

Pattern_groupindex_lookup looks up a named group in re.Pattern.groupindex; returns its 1-based group number, or -1 if no group by that name exists in this pattern.

Pattern_groupindex_count/Pattern_groupindex_at enumerate groupindex as a whole (Python: len(pattern.groupindex), dict(pattern.groupindex).items()): _count returns the number of named groups; _at(self, i, &name) for 0 <= i < count writes a borrowed pointer (valid as long as self is) to the i-th name into *name and returns its 1-based group number, or returns -1 and leaves *name untouched for an out-of-range i. Enumeration order is declaration order in the pattern source, which is not something Python's own dict-based groupindex guarantees at the language level but was verified to match in practice (insertion order) against a real CPython 3.11 interpreter ((?P<c>c)(?P<a>a)(?P<b>b)'s groupindex.items() lists c, a, b, not alphabetical order).

3.12-3.20 Module level functions

int re_match(const char *pattern, size_t len, int flags, Input *string, Match *out);
int re_fullmatch(const char *pattern, size_t len, int flags, Input *string, Match *out);
int re_search(const char *pattern, size_t len, int flags, Input *string, Match *out);
int re_finditer(const char *pattern, size_t len, int flags, Input *string, MatchIterCb cb, void *ctx);
int re_findall(const char *pattern, size_t len, int flags, Input *string, MatchIterCb cb, void *ctx);
int re_split(const char *pattern, size_t len, int flags, Input *string, int maxsplit, MatchIterCb cb, void *ctx);
int re_sub(const char *pattern, size_t len, int flags, Input *string, const char *repl, MatchSubCb cb, void *ctx, int count, char **out, size_t *outlen);
int re_subn(const char *pattern, size_t len, int flags, Input *string, const char *repl, MatchSubCb cb, void *ctx, int count, char **out, size_t *outlen, int *n);

Each compiles pattern through an internal cache and then calls the matching Pattern_ function with pos=0, endpos=-1 (module level re.match/re.search/etc. do not expose pos/endpos either, only the Pattern methods do, matching CPython exactly), exactly as CPython's own re/__init__.py implements re.match as _compile(pattern, flags).match(string). The cache is keyed by (pattern, len, flags), holds up to 512 entries, and is cleared entirely on overflow rather than evicting individual entries (mirroring the strategy CPython's own re module cache uses). A compile error inside these functions is silently reported as a -1 return, with no PatternError available (Section 1.4); use re_compile plus a Pattern_ function directly whenever a compile error needs to be diagnosed, or whenever the same pattern is applied more than once (idiomatic Python precompiles a pattern reused in a loop rather than calling the module level function repeatedly, and so should idiomatic use of this API, concept.md 9.1).

Concurrency: the cache is shared, mutable, process-wide state with no internal locking (concept.md 13.6). Do not call any re_-prefixed module level function from more than one thread without external synchronization; Pattern_-prefixed functions on a Pattern no thread is concurrently modifying (which is all of them, since nothing here mutates a compiled Pattern) have no such restriction.

3.21 re_purge

void re_purge(void);

Clears the module level cache, matching re.purge() exactly, including that it has no effect on any Pattern * a caller already holds a direct reference to (only the cache entry is dropped; already-returned pointers remain valid until Pattern_freed).

3.22 re_escape

void re_escape(const char *in, size_t len, char **out, size_t *outlen);

Matches re.escape exactly, including the narrowed escaped-character set CPython adopted in 3.7 (only characters that are actually special in a regex, plus non-ASCII bytes are passed through unescaped rather than every non-alphanumeric character as in pre-3.7 Python). *out is a freshly malloc'd buffer the caller must free.

3.23-3.29 Match_ accessors

int         Match_group(Match *self, const char *name_or_null, int index, const char **out, size_t *outlen);
int64_t     Match_start(Match *self, int group);
int64_t     Match_end(Match *self, int group);
void        Match_span(Match *self, int group, int64_t *start, int64_t *end);
int64_t     Match_start_byte(Match *self, int group);
int64_t     Match_end_byte(Match *self, int group);
void        Match_span_byte(Match *self, int group, int64_t *start, int64_t *end);
void        Match_free(Match *self);

Match_group: if name_or_null is non-NULL, index is ignored and the group is looked up by name (via the same table Pattern_groupindex_lookup uses); otherwise index (0 for the whole match) selects the group directly. Returns 1 and sets *out/*outlen to a borrowed pointer into the underlying Input's buffer (valid as long as both the Match and the Input are) when the group matched; returns 0 and sets *out = NULL, *outlen = 0 when the group exists but did not participate (Python's None); returns -1, leaving *out/*outlen untouched, when no such group exists at all (by index or by name).

Match_start/Match_end/Match_span: report the group's span in the pattern's native unit (code point index in UTF8 mode, byte offset otherwise). Deviation from CPython: Python's Match.start(group)/.end(group) raise IndexError for an out-of-range group number and return -1 only for a valid, unparticipated group; this build returns -1 for both cases uniformly, since C has no exception to raise. A caller that must tell "no such group" apart from "this group did not participate" should use Match_group instead, which does distinguish them (-1 versus 0 above).

Match_start_byte/Match_end_byte/Match_span_byte: always report a byte offset into the Input, regardless of encoding mode; identical to the non-_byte accessors in BINARY/ASCII mode, and the byte-offset translation of the same span in UTF8 mode. Use these, not the code-point ones, whenever the result will be used to slice or seek into the raw Input buffer or file (Match_group already does this translation internally, so most callers only need these directly for cases Match_group does not cover, such as reporting a byte offset to an external tool).

Match_free: frees the private per-match state (capture slots and, in UTF8 mode, the code-point-to-byte-offset table). Does not free the Match struct itself, and does not affect Match.re/Match.string (borrowed, Section 1.2). Safe to call more than once on the same Match (the second call is a no-op, since the first sets slots to NULL).

4. Memory ownership summary

Object Created by Freed by Notes
Pattern * re_compile Pattern_free Never returned by the re_-prefixed module level functions; those only take a pattern string, they do not hand back the Pattern they compiled internally.
Input * Input_from_buffer / Input_from_file Input_free Input_from_buffer never owns the wrapped buffer; Input_from_file always owns the buffer it read.
Match (the struct) The caller (stack or heap) The caller Never allocated by the library; only its private slots are, released by Match_free.
PatternError (the struct) The caller (stack or heap) The caller Only msg/pattern are heap allocated; release them with PatternError_free.
*out from Pattern_sub/subn, re_escape, and a MatchSubCb's own *out The library (or, for the callback, the callback itself) The caller (or, for the callback's *out, Pattern_sub/subn, immediately after copying it) Plain malloc'd buffers; free() them normally.
Text returned by Match_group Borrowed from the Input's buffer Nobody (not a separate allocation) Valid exactly as long as the Input and the Match both are.

5. Rejected constructs

These fail re_compile with a PatternError naming the construct, rather than being silently mis-parsed. See concept.md Section 4/13 for why each is out of scope for this build specifically (as opposed to out of scope for Python re, Section 6 below).

  • Conditional groups: (?(id)yes|no), (?(name)yes|no).
  • Scoped inline flags: (?i:...), (?imsx-imsx:...) (global inline flags at the very start of the pattern, (?aiLmsux), are supported).
  • Named code points: \N{NAME}.
  • Variable-width lookbehind: (?<=...)/(?<!...) whose body is not a single, statically known width (matches CPython's own restriction, not an additional one this build adds).

6. What is not Python re syntax at all

POSIX bracket-expression syntax ([[:alpha:]], [.collating-symbol.], [=equivalence-class=]) is rejected the same way, but for a different reason: it is not part of Python re's grammar in the first place (concept.md Section 14 is the complete comparison against POSIX regex.h, for a reader coming from C who might otherwise expect it).

See also

  • ../concept.md: the full design document (why a two-engine split was planned, the automata-theory argument for it, the POSIX comparison).
  • ../README.md: build instructions, the rxgrep example, and the "Implementation status" summary this document expands on.