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
-4
View File
@@ -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
+21
View File
@@ -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
+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
+10
View File
@@ -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;
+12 -4
View File
@@ -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);