Each new program under examples/ isolates one distinct feature rather than being a general purpose tool like the existing rxgrep.c: binary_scan.c (raw byte-range classes including an embedded NUL and an embedded 0x0A), utf8_scripts.c (\w across Latin/Greek/Cyrillic/CJK text, code point versus byte offsets), ascii_logparse.c (named groups against structured log text), redos_atomic.c (atomic groups and possessive quantifiers timed directly against the unprotected form of the textbook (a+)+b ReDoS shape), empty_match_rule.c (CPython's undocumented empty-match retry rule, verified: \d*? against "123abc456" gives 16 matches, not 9), and large_file_search.c (Input_from_file's mmap-backed reading on a generated 100MB file, with elapsed time and peak RSS printed). Every example was compiled and run while writing it; the claims in each file's top comment are checked against its own output, not written by hand and left unverified. Also adds examples/bench_vs_posix.c, a direct, honestly reported comparison against the C standard library's own <regex.h> (regcomp/regexec) on six scenarios at multi-megabyte or multi-hundred-thousand-line scale, using only pattern syntax valid for both engines so they run the identical pattern text. glibc's DFA-backed engine wins five of six scenarios by 2x-35x, which is the expected outcome of a roughly 2000-line backtracking interpreter built for Python `re` compatibility competing against a mature, heavily optimized engine with a much smaller feature set; the sixth scenario has no POSIX equivalent at all (an atomic group). Every scenario's match count is cross-checked between the two engines as an independent correctness signal beyond the existing CPython-derived test suite. Two real issues were found and fixed while building this benchmark, not left in: iterating regexec() over an advancing string pointer is quadratic in practice (no way to bound the search without an implicit NUL-scan on every call), fixed by using REG_STARTEND instead; and a signed integer overflow (undefined behavior, caught by UBSan) in the benchmark's own pseudo-random text generator, fixed by using an unsigned accumulator. README.md and USAGE.md gain pointers to examples/README.md (the new per-example index) and a "Benchmarks" section summarizing the POSIX comparison honestly, including where it loses. The Makefile gains a `make examples` target building all seven programs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY
101 lines
3.9 KiB
C
101 lines
3.9 KiB
C
/* empty_match_rule - demonstrates a correctness feature rather than a
|
|
* performance one: Pattern_finditer/Pattern_split reproduce CPython's own
|
|
* undocumented empty-match behavior exactly, not the simpler "skip forward
|
|
* one position after an empty match" rule a first implementation attempt
|
|
* would reach for.
|
|
*
|
|
* A real CPython interpreter, if the match found at a given start position
|
|
* is empty, additionally searches for and reports a second, non-empty
|
|
* match at that same start position before moving on, rather than only
|
|
* reporting the empty one. This was found by directly probing a real
|
|
* Python interpreter (docs/API.md Section 3.5-3.6, README.md "Testing"),
|
|
* not by reading CPython's documentation, which does not describe this
|
|
* rule; an earlier revision of this library's Pattern_finditer/split did
|
|
* not implement it and was corrected once the discrepancy against real
|
|
* CPython output was found by the combinatorial test suite.
|
|
*
|
|
* The canonical example: \d*? (a lazy, possibly-empty run of digits)
|
|
* against "123abc456" finds 16 matches with a real CPython interpreter,
|
|
* not 9 (which is what "one match per start position, non-overlapping"
|
|
* alone would produce for a 9-character subject).
|
|
*/
|
|
#include "regexx.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
typedef struct { int n; } Ctx;
|
|
|
|
static void print_match(void *ctx, const Match *m_const) {
|
|
Ctx *c = ctx;
|
|
Match *m = (Match *)m_const;
|
|
const char *g; size_t glen;
|
|
Match_group(m, NULL, 0, &g, &glen);
|
|
int64_t s, e;
|
|
Match_span(m, 0, &s, &e);
|
|
printf(" match %2d: span=[%lld,%lld) text='%.*s'\n",
|
|
c->n, (long long)s, (long long)e, (int)glen, g);
|
|
c->n++;
|
|
}
|
|
|
|
static void print_split_elem(void *ctx, const Match *m_const) {
|
|
Ctx *c = ctx;
|
|
Match *m = (Match *)m_const;
|
|
const char *g; size_t glen;
|
|
int r = Match_group(m, NULL, 0, &g, &glen);
|
|
if (r == 1) printf(" elem %2d: '%.*s'\n", c->n, (int)glen, g);
|
|
else printf(" elem %2d: (None)\n", c->n);
|
|
c->n++;
|
|
}
|
|
|
|
int main(void) {
|
|
const char *pattern = "\\d*?";
|
|
const char *subject = "123abc456";
|
|
|
|
PatternError err; memset(&err, 0, sizeof err);
|
|
Pattern *pat = re_compile(pattern, strlen(pattern), ASCII, &err);
|
|
if (!pat) {
|
|
fprintf(stderr, "compile error: %s\n", err.msg);
|
|
PatternError_free(&err);
|
|
return 1;
|
|
}
|
|
Input *in = Input_from_buffer((const uint8_t *)subject, strlen(subject));
|
|
|
|
printf("Pattern_finditer(\"%s\", \"%s\"):\n", pattern, subject);
|
|
Ctx ctx = { 0 };
|
|
int n = Pattern_finditer(pat, in, 0, -1, print_match, &ctx);
|
|
printf("total: %d matches (a real CPython interpreter reports 16 for\n"
|
|
"this exact pattern/subject pair, not 9; this build matches it)\n\n", n);
|
|
|
|
printf("Pattern_split(\"\\\\s*,\\\\s*\", \"a, b,c\") for contrast (an ordinary,\n"
|
|
"always-nonempty separator, no empty-match retry needed): \n");
|
|
{
|
|
const char *sp = "\\s*,\\s*";
|
|
PatternError err2; memset(&err2, 0, sizeof err2);
|
|
Pattern *pat2 = re_compile(sp, strlen(sp), ASCII, &err2);
|
|
const char *subj2 = "a, b,c";
|
|
Input *in2 = Input_from_buffer((const uint8_t *)subj2, strlen(subj2));
|
|
Ctx ctx2 = { 0 };
|
|
Pattern_split(pat2, in2, 0, print_split_elem, &ctx2);
|
|
Input_free(in2);
|
|
Pattern_free(pat2);
|
|
}
|
|
|
|
printf("\nPattern_split(\"x*\", \"-abc-\") (a pattern that CAN match empty,\n"
|
|
"showing the same retry rule apply during split):\n");
|
|
{
|
|
const char *sp = "x*";
|
|
PatternError err3; memset(&err3, 0, sizeof err3);
|
|
Pattern *pat3 = re_compile(sp, strlen(sp), ASCII, &err3);
|
|
const char *subj3 = "-abc-";
|
|
Input *in3 = Input_from_buffer((const uint8_t *)subj3, strlen(subj3));
|
|
Ctx ctx3 = { 0 };
|
|
Pattern_split(pat3, in3, 0, print_split_elem, &ctx3);
|
|
Input_free(in3);
|
|
Pattern_free(pat3);
|
|
}
|
|
|
|
Input_free(in);
|
|
Pattern_free(pat);
|
|
return 0;
|
|
}
|