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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY
This commit is contained in:
2026-09-14 12:23:56 +00:00
co-authored by Claude Sonnet 5
parent 5ae68d4ea6
commit 09718eaaf6
5 changed files with 50 additions and 11 deletions
+7 -3
View File
@@ -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 (`(?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