concept.md is the full design document: the objective (Python re parity plus binary/ASCII/UTF-8 modes, gigabyte-scale input, single C file), the automata-theory argument for why unrestricted backreferences/lookaround are incompatible with strict single-pass constant memory, the resulting two-engine architecture, the exact Python-mirroring naming convention, and a full comparison against POSIX regex.h for C-background readers. regexx.c/regexx.h are the v1 implementation: parser, compiler to a Pike/backtracking-style bytecode, and a single recursive backtracking engine covering the pattern syntax and operations listed in README.md, validated against CPython's own re module output (tests/), clean under AddressSanitizer/UBSan, and stress-tested (50MB simple-quantifier match, graceful failure rather than a crash on complex repeats over large input, clean rejection of every intentionally unsupported construct). Also included: examples/rxgrep.c (a small grep-like program exercising all three data modes and the substitution API), the Makefile, the MIT LICENSE, and docs/API.md, an exhaustive reference for every type, flag, and function's exact return-value and memory-ownership convention, checked against the current source and against a real CPython interpreter rather than against memory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY
182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate tests/generated_tests.c from tests/cases.py, using CPython's
|
|
own `re` module as ground truth (concept.md Section 11)."""
|
|
import re
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from cases import CASES # noqa: E402
|
|
|
|
FLAGMAP = {
|
|
"IGNORECASE": re.IGNORECASE,
|
|
"MULTILINE": re.MULTILINE,
|
|
"DOTALL": re.DOTALL,
|
|
"VERBOSE": re.VERBOSE,
|
|
"LOCALE": re.LOCALE,
|
|
"ASCII": re.ASCII,
|
|
}
|
|
|
|
|
|
def c_str(b):
|
|
if isinstance(b, str):
|
|
b = b.encode("utf-8")
|
|
out = ['"']
|
|
for byte in b:
|
|
c = chr(byte)
|
|
if c == '"':
|
|
out.append('\\"')
|
|
elif c == "\\":
|
|
out.append("\\\\")
|
|
elif 32 <= byte < 127:
|
|
out.append(c)
|
|
else:
|
|
out.append("\\%03o" % byte)
|
|
out.append('"')
|
|
return "".join(out)
|
|
|
|
|
|
def py_flags(case):
|
|
f = 0
|
|
if case["mode"] == "ascii":
|
|
f |= re.ASCII
|
|
for name in case["flags"]:
|
|
f |= FLAGMAP[name]
|
|
return f
|
|
|
|
|
|
def c_flags(case):
|
|
names = []
|
|
if case["mode"] == "ascii":
|
|
names.append("ASCII")
|
|
elif case["mode"] == "utf8":
|
|
names.append("UTF8")
|
|
elif case["mode"] == "binary":
|
|
names.append("BINARY")
|
|
for name in case["flags"]:
|
|
names.append(name)
|
|
return "|".join(names) if names else "0"
|
|
|
|
|
|
def encode(case, s):
|
|
if case["mode"] == "binary":
|
|
return s.encode("utf-8")
|
|
return s
|
|
|
|
|
|
def blen(case, s):
|
|
"""Byte length of the encoded subject, as the C API always wants a
|
|
byte count (UTF8 mode still stores a byte buffer; only Match_start
|
|
reports code points), never a Python character count."""
|
|
e = encode(case, s)
|
|
return len(e) if isinstance(e, bytes) else len(e.encode("utf-8"))
|
|
|
|
|
|
def desc(case, i):
|
|
return "#%d %s /%s/ on %r" % (i, case["op"], case["pattern"], case["subject"])
|
|
|
|
|
|
def gen_search_family(case, i, out):
|
|
kind = {"search": 0, "match": 1, "fullmatch": 2}[case["op"]]
|
|
flags = py_flags(case)
|
|
subj = encode(case, case["subject"])
|
|
pat = encode(case, case["pattern"])
|
|
compiled = re.compile(pat, flags)
|
|
fn = {"search": compiled.search, "match": compiled.match, "fullmatch": compiled.fullmatch}[case["op"]]
|
|
m = fn(subj)
|
|
d = desc(case, i)
|
|
if m is None:
|
|
out.append(' tc_search_family(%s, %s, %s, %s, %d, %d, 0, 0, NULL);' % (
|
|
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), kind))
|
|
return
|
|
spans = [m.start(0), m.end(0)]
|
|
for g in range(1, compiled.groups + 1):
|
|
try:
|
|
spans += [m.start(g), m.end(g)]
|
|
except IndexError:
|
|
spans += [-1, -1]
|
|
n_spans = compiled.groups + 1
|
|
arr = ",".join(str(v) for v in spans)
|
|
out.append(' { static const long long sp[] = {%s}; tc_search_family(%s, %s, %s, %s, %d, %d, 1, %d, sp); }' % (
|
|
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), kind, n_spans))
|
|
|
|
|
|
def gen_finditer(case, i, out):
|
|
flags = py_flags(case)
|
|
subj = encode(case, case["subject"])
|
|
pat = encode(case, case["pattern"])
|
|
ms = list(re.finditer(pat, subj, flags))
|
|
spans = []
|
|
for m in ms:
|
|
spans += [m.start(0), m.end(0)]
|
|
d = desc(case, i)
|
|
if not spans:
|
|
out.append(' tc_finditer(%s, %s, %s, %s, %d, 0, NULL);' % (
|
|
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"])))
|
|
return
|
|
arr = ",".join(str(v) for v in spans)
|
|
out.append(' { static const long long sp[] = {%s}; tc_finditer(%s, %s, %s, %s, %d, %d, sp); }' % (
|
|
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), len(ms)))
|
|
|
|
|
|
def gen_sub(case, i, out):
|
|
flags = py_flags(case)
|
|
subj = encode(case, case["subject"])
|
|
pat = encode(case, case["pattern"])
|
|
repl = encode(case, case["repl"])
|
|
count = case.get("count", 0)
|
|
result, n = re.subn(pat, repl, subj, count=count, flags=flags)
|
|
d = desc(case, i)
|
|
result_str = result.decode("utf-8") if isinstance(result, bytes) else result
|
|
out.append(' tc_sub(%s, %s, %s, %s, %d, %s, %d, %s, %d);' % (
|
|
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]),
|
|
c_str(case["repl"]), count, c_str(result_str), n))
|
|
|
|
|
|
def gen_split(case, i, out):
|
|
flags = py_flags(case)
|
|
subj = encode(case, case["subject"])
|
|
pat = encode(case, case["pattern"])
|
|
maxsplit = case.get("maxsplit", 0)
|
|
items = re.split(pat, subj, maxsplit=maxsplit, flags=flags)
|
|
d = desc(case, i)
|
|
parts = []
|
|
for it in items:
|
|
if it is None:
|
|
parts.append("NULL")
|
|
else:
|
|
s = it.decode("utf-8") if isinstance(it, bytes) else it
|
|
parts.append(c_str(s))
|
|
arr = ",".join(parts) if parts else "0"
|
|
out.append(' { static const char *const items[] = {%s}; tc_split(%s, %s, %s, %s, %d, %d, %d, items); }' % (
|
|
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]),
|
|
maxsplit, len(items)))
|
|
|
|
|
|
def main():
|
|
out = []
|
|
out.append('/* GENERATED by tests/gen.py from tests/cases.py. Do not edit by hand. */')
|
|
out.append('#include "harness.h"')
|
|
out.append('void run_generated_tests(void) {')
|
|
for i, case in enumerate(CASES):
|
|
op = case["op"]
|
|
if op in ("search", "match", "fullmatch"):
|
|
gen_search_family(case, i, out)
|
|
elif op == "finditer":
|
|
gen_finditer(case, i, out)
|
|
elif op == "sub":
|
|
gen_sub(case, i, out)
|
|
elif op == "split":
|
|
gen_split(case, i, out)
|
|
else:
|
|
raise ValueError("unknown op %r" % op)
|
|
out.append('}')
|
|
dest = os.path.join(os.path.dirname(__file__), "generated_tests.c")
|
|
with open(dest, "w") as f:
|
|
f.write("\n".join(out) + "\n")
|
|
print("wrote %s (%d cases)" % (dest, len(CASES)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|