From 09718eaaf60de03ac35e8e4dd33320d42749972c Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 14 Sep 2026 12:23:56 +0000 Subject: [PATCH] Add Pattern_groupindex_count/_at, closing the groupindex enumeration gap README.md and docs/API.md both previously documented "no way to list every name a pattern defines without already knowing what to look for" as an accepted limitation of Pattern_groupindex_lookup being the only access to groupindex. It was not actually a hard constraint: the underlying GroupIndex struct (regexx.c) already stores every name and its group number in two parallel arrays, populated once at compile time; only a public accessor was missing. Pattern_groupindex_count returns the number of named groups; Pattern_groupindex_at(self, i, &name) for 0 <= i < count writes the i-th name and returns its 1-based group number, or returns -1 for an out-of-range i. Enumeration order is declaration order, verified against a real CPython 3.11 interpreter to match groupindex's own practical (insertion-order) iteration order, not just assumed. Verified directly (count/name/group-number correctness, matching the exact snippet now in USAGE.md's own output), full 3,252-case suite unaffected (3252/3252, this is a pure accessor addition touching no matching logic), clean AddressSanitizer/UndefinedBehaviorSanitizer. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY --- README.md | 4 ---- USAGE.md | 21 +++++++++++++++++++++ docs/API.md | 10 +++++++--- regexx.c | 10 ++++++++++ regexx.h | 16 ++++++++++++---- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fd54392..bd3b1cb 100644 --- a/README.md +++ b/README.md @@ -377,10 +377,6 @@ convention they follow. has no exception to raise. `Match_group` does distinguish them (`-1` for no such group, `0` for an unparticipated one), see `docs/API.md` Section 3.23. -- `Pattern.groupindex` has no enumeration function in this build, only - `Pattern_groupindex_lookup(pattern, name)`; there is no way to list every - name a compiled pattern defines without already knowing what to look for. - ## Building Requires a C11 compiler and, for the test suite, Python 3 (used only to diff --git a/USAGE.md b/USAGE.md index 88cbe5b..6d594af 100644 --- a/USAGE.md +++ b/USAGE.md @@ -210,6 +210,27 @@ is the only place in the API that tells "no such group" apart from "this group matched nothing" (`Match_start`/`Match_end` collapse both cases to `-1`, `docs/API.md` Section 3.23-3.29). +Every name a `Pattern` defines can also be enumerated, without already +knowing what to look for, via `Pattern_groupindex_count`/`_at` (Python: +`len(pattern.groupindex)`, `dict(pattern.groupindex).items()`): + +```c +int n = Pattern_groupindex_count(pat); +for (int i = 0; i < n; i++) { + const char *name; + int gnum = Pattern_groupindex_at(pat, i, &name); + printf(" [%d] name=%s group=%d\n", i, name, gnum); +} +``` + +Output, verified, for the same `pat` above: + +``` +count=2 + [0] name=user group=1 + [1] name=host group=2 +``` + ## 6. Code points versus bytes in `UTF8` mode In `UTF8` mode, `Match_start`/`Match_end`/`Match_span` report a code point diff --git a/docs/API.md b/docs/API.md index 5c33c57..df3c558 100644 --- a/docs/API.md +++ b/docs/API.md @@ -29,7 +29,7 @@ struct Pattern { - `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` (3.11) for the only supported access to it. +- `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`. @@ -191,13 +191,17 @@ 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` +### 3.11 `Pattern_groupindex_lookup` / `_count` / `_at` ```c 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); ``` -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. This is the only supported access to `groupindex`'s content in this build: there is no enumeration function (no way to list every name a pattern defines without already knowing what to look for), unlike Python's `groupindex`, which is a full mapping object supporting iteration and `len()`. A caller that needs every name a pattern uses must track the names it compiled the pattern with itself. +`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 (`(?Pc)(?Pa)(?Pb)`'s `groupindex.items()` lists `c`, `a`, `b`, not alphabetical order). ### 3.12-3.20 Module level functions diff --git a/regexx.c b/regexx.c index 4dd655b..072b8c9 100644 --- a/regexx.c +++ b/regexx.c @@ -2295,6 +2295,16 @@ int Pattern_groupindex_lookup(Pattern *self, const char *name) { PatternImpl *impl = (PatternImpl *)self->program; return groupindex_lookup(&impl->gidx, name); } +int Pattern_groupindex_count(Pattern *self) { + PatternImpl *impl = (PatternImpl *)self->program; + return impl->gidx.n; +} +int Pattern_groupindex_at(Pattern *self, int i, const char **name) { + PatternImpl *impl = (PatternImpl *)self->program; + if (i < 0 || i >= impl->gidx.n) return -1; + *name = impl->gidx.names[i]; + return impl->gidx.idx[i]; +} int Match_group(Match *self, const char *name_or_null, int index, const char **out, size_t *outlen) { MatchSlots *ms = self->slots; diff --git a/regexx.h b/regexx.h index 8893d8d..226d528 100644 --- a/regexx.h +++ b/regexx.h @@ -107,11 +107,19 @@ int Pattern_subn(Pattern *self, Input *string, const char *repl, MatchSubCb void Pattern_free(Pattern *self); /* Looks up a named group in re.Pattern.groupindex; returns its 1-based - * group number, or -1 if no group by that name exists. There is no - * enumeration function for groupindex as a whole in this build: a - * caller that needs every name must track the names it used to build - * the pattern itself, since Pattern->groupindex is otherwise opaque. */ + * group number, or -1 if no group by that name exists. */ int Pattern_groupindex_lookup(Pattern *self, const char *name); +/* Enumerates re.Pattern.groupindex as a whole (Python: len(pattern. + * groupindex), dict(pattern.groupindex).items()), in declaration order + * (Python's own dict does not guarantee an order at the language level, + * but in practice preserves insertion order the same way this does). + * Pattern_groupindex_count returns the number of named groups. + * Pattern_groupindex_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; out of range i returns + * -1 and leaves *name untouched. */ +int Pattern_groupindex_count(Pattern *self); +int Pattern_groupindex_at(Pattern *self, int i, const char **name); /* ---- Match accessors ---- */ int Match_group(Match *self, const char *name_or_null, int index, const char **out, size_t *outlen);