Skip to content

Read the match shapes from the capture shape analysis - #4

Closed
yoeunes wants to merge 2 commits into
judge-pattern-validity-for-the-analysed-enginefrom
consume-the-capture-shape
Closed

yoeunes wants to merge 2 commits into
judge-pattern-validity-for-the-analysed-enginefrom
consume-the-capture-shape

Conversation

@yoeunes

@yoeunes yoeunes commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3.

What

The capturing groups and the whole match now take their types from the capture shape analysis the parser computes beside the patterns it reads. Only the topology stays in RegexGroupParser: which group sits in which branch of which alternation, under which optional quantifier.

Topology rules that mirror what the matcher folds:

  • an optional quantifier marks the one node it wraps, and only that one — a group further below learns its optionality from its parent chain;
  • a capturing parent carries its optionality to its children;
  • a group a (?(DEFINE)...) block defines holds the empty string alone: it is numbered, it is optional, and nothing more is true of it;
  • the branches of a conditional run one at a time and are read as exclusive as an alternation's, and a lookaround condition counts its groups before them.

The subject narrows on its own channel. $matches[0] is the match the engine wrote, constants included; the subject merely contains the match, so it narrows to the accessories the match leaves open — never to the match's constants or digits, and $ admits one final newline ("a\n" matches /^(a|b|c)$/).

Against the engine

PHPRegexBackendParityTest matches every pattern family for real and checks what preg_match() writes into $matches against the shape: the group count, the values, the optionality of what the engine leaves unset. About 200 assertions across 17 nsrt fixtures re-pin what the sharper shapes read:

  • constant unions for literals and alternations ('a'|'b'|'c', 'foobar'|'foobarbaz');
  • numeric-string for digit runs, '0'|'1'|…|'9' for single digits;
  • non-falsy-string where a mandatory neighbour pins the value off zero (3(/.*)?, (c(\d+)?)?, (\w+)z);
  • the empty string alone where a zero-width first branch wins the match ((^|foo) reads '').

NodeScopeResolverTest: 1879/1879. Static analysis of the repository: 0 errors. Known imprecision, documented here rather than hidden: a lone (?(DEFINE)...) group renders as a union of the empty match and the match with the defined group at '' — both arms true, the second never taken by the engine.

Packaging

The Hoa retreat's residue leaves with this pull request too: the autoload fix in bin/phpstan, the scoper stub and the Hoa prefix exclusion. The shapes cache on the judged engine, and the ignored-error validator reads its patterns with their modifiers, so /x patterns in ignoreErrors no longer read as if their blanks were literal.

The group types and the whole-match type now come from the capture
shape analysis the parser computes beside the patterns it reads, and
only the topology - which group sits in which branch of which
alternation, under which optional quantifier - is walked here. An
optional quantifier marks the one node it wraps the way the matcher
reads it, a capturing parent carries its optionality to its children,
and a group a (?(DEFINE)...) block defines stays optional forever.

The subject narrows on its own channel: $matches[0] is the match the
engine wrote, constants included, while the subject contains the match
and narrows to the accessories the match leaves open - never to the
match's constants or digits, and $ admits one final newline.

The fixtures re-pin what the sharper shapes read: constants for
literals and alternations, numeric-string for digit runs,
non-falsy-string for values a mandatory neighbour pins off zero.
…olicy, the packaging residue

The conditional branches run one at a time and are read as exclusive as
an alternation's; a lookaround condition counts its groups before
them, and a group a (?(DEFINE)...) block defines holds the empty string
alone — what preg_match writes is pinned in PHPRegexBackendParityTest,
compared against the engine itself.

The judged engine now follows the pin then the witness: a named engine
is a statement honoured in both directions, and the running engine
speaks for the analysed PHP when the two share a minor, so a patch
release carries the engine it was cut with instead of its minor's
first. A release candidate reads as its release, and a broken pin or
platform value fails the boot naming the knob to fix.

The shapes cache on the judged engine, the ignored-error validator
reads its patterns with their modifiers, and the packaging no longer
carries what the Hoa retreat left: the autoload fix in bin/phpstan and
the scoper stubs and exclusions.
@yoeunes

yoeunes commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Folded into #1: the stack now travels as one pull request while the series is being built. The branch content is unchanged — read-regex-shapes-through-phpregex points at the same commits.

@yoeunes yoeunes closed this Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant