Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Regular Expressions  Module 4 – Quantifiers: greedy, lazy and possessive

Possessive quantifiers and atomic groups

How a++ and (?>...) throw away the positions an engine comes back to, which flavours accept them, when they change a match, and what JavaScript does.

  • Advanced
  • 25 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how a possessive quantifier and an atomic group throw away the positions an engine would come back to
  • Predict when a*+a and (?>a|ab)c refuse text that the plain forms match
  • Rewrite a repetition-heavy pattern in Python, PCRE2 or PHP and check that it keeps the same answers
  • Write the JavaScript lookahead and backreference that lock a repetition up
  • State which flavours accept each form, and which quantifiers PCRE2 possessifies by itself

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a possessive quantifier and an atomic group throw away the positions an engine would come back to
  • Predict when a*+a and (?>a|ab)c refuse text that the plain forms match
  • Rewrite a repetition-heavy pattern in Python, PCRE2 or PHP and check that it keeps the same answers
  • Write the JavaScript lookahead and backreference that lock a repetition up
  • State which flavours accept each form, and which quantifiers PCRE2 possessifies by itself

Before you start

On this page

One pattern for a list of numeric ids, ^(?:\d+,?)+$, needed 1,572,864 turns of PCRE2’s matching loop to refuse twenty digits with a stray character at the end, and the same pattern with two extra plus signs needed 4. Both answer every valid list the same way. The two extra signs are possessive quantifier: A quantifier with an extra plus sign, such as x*+, x++, x?+ or x{m,n}+, that matches as much as a greedy one and then keeps it: it saves no shorter run, so a failure later in the pattern never makes it give characters back., the simplest way to tell an engine that a repetition is final.

The sections below walk through what a possessive quantifier and an atomic group: A group written (?>...) that keeps the first match its own pattern finds: a failure later in the pattern cannot backtrack into it to make it try another way. x++ means the same as (?>x+). throw away, count the work they save with PCRE2’s own counters, show the cases where they change the answer rather than the speed, and end with the lookahead that gives JavaScript the same effect without the syntax.

A quantifier that keeps everything it took

Take the subject aaa and two patterns that look almost the same: a+a and a++a. The first matches all three characters. The second matches nothing at all. A plain greedy quantifier: A quantifier in its usual form, such as * or {2,5}, which repeats its atom as many times as it can and gives repetitions back one at a time only when the rest of the pattern cannot match otherwise. More in “Greedy and lazy quantifiers” takes as much as it can and then, if the rest of the pattern fails, hands characters back one at a time; this is backtracking: How a backtracking engine reacts to a failure: it returns to its most recent choice point, restores the text position it had there, and tries the next alternative. More in “How an engine finds a match, step by step”, and every position it could return to is a choice point: A place in a pattern with more than one way to go on, such as an alternation. The engine takes the first way and saves the others so it can come back to them. More in “How an engine finds a match, step by step” the engine has to remember. A possessive quantifier takes as much as it can and keeps no such positions, so a failure after it ends the attempt at that starting point.

Step through both walks on the same three characters, and watch the single moment where they part:

Interactive · step by step What a plain quantifier gives back, and what a possessive one keeps Step through both patterns on the same three characters: which characters the quantifier takes, which one it hands back, and why the second walk has nothing to hand back.
What a plain quantifier gives back, and what a possessive one keeps, step 8 of 8. Showing a at index 0 (kept by a++), a at index 1 (kept by a++), a at index 2 (kept by a++), a++a: no match. In focus: a++a: no match.a at index 0kept by a++a at index 1kept by a++a at index 2kept by a++a++a: no match

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Three characters, two patterns. The subject is aaa. Both patterns ask for a run of a characters and then one more a: the first writes that run as a+, the second as a++.
  2. a+ takes all three. A plain + is greedy, so a+ takes every a it can reach: index 0, then 1, then 2. The engine notes, at each step, that it could have stopped earlier.
  3. The last a of the pattern fails. Nothing is left of the subject, so the final a of the pattern has no character to match. This is the moment that decides the rest of the walk.
  4. One character goes back. The engine returns to its most recent note, where a+ had taken only two characters, and hands index 2 back to the pattern.
  5. Now the final a matches. Index 2 is unclaimed again, so the final a of the pattern takes it. a+a matches all three characters, because one of them was given up and taken again.
  6. a++ takes the same three. The second walk starts over. a++ takes the same three characters, and here is the difference: it keeps no note of the shorter runs it could have taken.
  7. Nothing to hand back. The final a of the pattern fails again. This time there is no note to return to, so index 2 stays with a++ and the attempt at this starting point is over.
  8. No match anywhere. The engine tries the next two starting points, index 1 and index 2, and each one ends the same way. a*+a and the atomic group (?>a+)a answer no match on aaa for the same reason.

The possessive forms are written with one extra plus sign after the quantifier: a*+, a++, a?+ and a{3,5}+. The three tabs below run the same five pairs in CPython, in PHP and in PCRE2, and all three agree: a++a and a*+a find nothing in aaa, while a++b still matches aaab, because there the characters the quantifier kept are followed by the character the pattern wants next.

Five pairs of patterns, plain and possessive, on the same subjects

Python 3.14.8 · keeps/keeps.py

"""What a possessive quantifier keeps, in CPython 3.14.8 (possessive forms need 3.11 or later).

Each pair runs the same subject through a plain quantifier and its possessive form, so the only difference in a
line is the extra + after the quantifier. 'no match' means re.search found nothing at all.
"""

import re

PAIRS = [
    (r"a+a", r"a++a", "aaa"),
    (r"a*a", r"a*+a", "aaa"),
    (r"a+b", r"a++b", "aaab"),
    (r"a{3,5}aa", r"a{3,5}+aa", "aaaaaa"),
    (r'"[^"]*"', r'"[^"]*+"', 'say "yes" twice'),
]


def found(pattern: str, subject: str) -> str:
    """The matched text as a quoted string, or 'no match'."""
    hit = re.search(pattern, subject)
    return repr(hit.group(0)) if hit else "no match"


print("subject            pattern      match        possessive   match")
print("-" * 68)
for plain, possessive, subject in PAIRS:
    print(f"{subject!r:18} {plain:12} {found(plain, subject):12} {possessive:12} {found(possessive, subject)}")

print()
print("The possessive forms are the atomic groups of the same items:")
for possessive, atomic in [(r"a*+", r"(?>a*)"), (r"a++", r"(?>a+)"), (r"a?+", r"(?>a?)"), (r"a{3,5}+", r"(?>a{3,5})")]:
    same = found(possessive + "a", "aaa") == found(atomic + "a", "aaa")
    print(f"  {possessive + 'a':12} and {atomic + 'a':14} give the same answer on 'aaa': {same}")

Output

subject            pattern      match        possessive   match
--------------------------------------------------------------------
'aaa'              a+a          'aaa'        a++a         no match
'aaa'              a*a          'aaa'        a*+a         no match
'aaab'             a+b          'aaab'       a++b         'aaab'
'aaaaaa'           a{3,5}aa     'aaaaaa'     a{3,5}+aa    no match
'say "yes" twice'  "[^"]*"      '"yes"'      "[^"]*+"     '"yes"'

The possessive forms are the atomic groups of the same items:
  a*+a         and (?>a*)a        give the same answer on 'aaa': True
  a++a         and (?>a+)a        give the same answer on 'aaa': True
  a?+a         and (?>a?)a        give the same answer on 'aaa': True
  a{3,5}+a     and (?>a{3,5})a    give the same answer on 'aaa': True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keeps.py

PHP 8.5.11 · keeps/keeps.php

<?php
// The same pairs in PHP 8.5.11, whose preg functions call the PCRE2 library that ships with it. Only the version
// number of that library is printed, never its release date.

$pairs = [
    ['/a+a/', '/a++a/', 'aaa'],
    ['/a*a/', '/a*+a/', 'aaa'],
    ['/a+b/', '/a++b/', 'aaab'],
    ['/a{3,5}aa/', '/a{3,5}+aa/', 'aaaaaa'],
    ['/"[^"]*"/', '/"[^"]*+"/', 'say "yes" twice'],
];

/** The matched text in quotes, or 'no match'. */
function found(string $pattern, string $subject): string
{
    return preg_match($pattern, $subject, $hit) === 1 ? "'{$hit[0]}'" : 'no match';
}

printf("PHP %s with PCRE2 %s\n\n", PHP_VERSION, strtok(PCRE_VERSION, ' '));
foreach ($pairs as [$plain, $possessive, $subject]) {
    printf("%-18s %-12s %-12s %-12s %s\n", "'{$subject}'", $plain, found($plain, $subject), $possessive, found($possessive, $subject));
}

Output

PHP 8.5.11 with PCRE2 10.44

'aaa'              /a+a/        'aaa'        /a++a/       no match
'aaa'              /a*a/        'aaa'        /a*+a/       no match
'aaab'             /a+b/        'aaab'       /a++b/       'aaab'
'aaaaaa'           /a{3,5}aa/   'aaaaaa'     /a{3,5}+aa/  no match
'say "yes" twice'  /"[^"]*"/    '"yes"'      /"[^"]*+"/   '"yes"'

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php keeps.php

PCRE2 10.49 · keeps/keeps_pcre2.py

"""The same pairs in PCRE2 10.49, run through pcre2test.

pcre2test prints each pattern line, each indented subject under it, and then " 0:" with the text the pattern matched,
or "No match". The no_auto_possess modifier is used here so that each pattern runs exactly as it is written: without
it PCRE2 would make some of these quantifiers possessive on its own.
"""

from pcre2_session import run

run(r'''
# a plain + gives characters back, so the last a of the pattern finds one
/a+a/no_auto_possess
    aaa

# the possessive ++ keeps all three a characters, and the last a has nothing left
/a++a/no_auto_possess
    aaa

/a*a/no_auto_possess
    aaa

/a*+a/no_auto_possess
    aaa

# nothing is given back here either, but b follows the a characters, so the match stands
/a++b/no_auto_possess
    aaab

/a{3,5}aa/no_auto_possess
    aaaaaa

/a{3,5}+aa/no_auto_possess
    aaaaaa

# a quoted field: the class cannot match the closing quote, so keeping everything is safe
/"[^"]*+"/no_auto_possess
    say "yes" twice
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
# a plain + gives characters back, so the last a of the pattern finds one
/a+a/no_auto_possess
    aaa
 0: aaa

# the possessive ++ keeps all three a characters, and the last a has nothing left
/a++a/no_auto_possess
    aaa
No match

/a*a/no_auto_possess
    aaa
 0: aaa

/a*+a/no_auto_possess
    aaa
No match

# nothing is given back here either, but b follows the a characters, so the match stands
/a++b/no_auto_possess
    aaab
 0: aaab

/a{3,5}aa/no_auto_possess
    aaaaaa
 0: aaaaaa

/a{3,5}+aa/no_auto_possess
    aaaaaa
No match

# a quoted field: the class cannot match the closing quote, so keeping everything is safe
/"[^"]*+"/no_auto_possess
    say "yes" twice
 0: "yes"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keeps_pcre2.py

keeps/pcre2_session.py

"""Runs a script of patterns and subjects through pcre2test, the test program of the PCRE2 10.49 library, and prints
what it printed.

pcre2test reads a pattern line such as /a.b/ (with modifiers after the closing slash, for example /a.b/dotall,utf),
then one subject per indented line, and prints each subject with " 0:" and the matched text (" 1:" and on for groups)
or "No match". A line starting with # is a comment and is printed as it is. Escapes in a subject such as \\r, \\n or
\\x{2028} stand for those characters (pcre2test's own subject escapes), so invisible characters can be tested.

The script is given in the example file itself, so the example's run record changes whenever the script does:

    from pcre2_session import run
    run(r'''
    /a.b/
        a\\rb
    ''')

The version banner and its release date are not printed (pcre2test -q); the session checks the version itself and
stops if this computer's pcre2test is not PCRE2 10.49, so a record never shows another version's behaviour.
"""

import re
import subprocess
import sys
import tempfile
from pathlib import Path

VERSION = "10.49"
_CANDIDATES = ("/opt/homebrew/bin/pcre2test", "/usr/local/bin/pcre2test")
_ENV = {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "LANG": "C.UTF-8", "TZ": "UTC"}


def _tool() -> str:
    for candidate in _CANDIDATES:
        if Path(candidate).is_file():
            return candidate
    sys.exit("pcre2test (PCRE2 10.49) was not found")


def _check_version(tool: str) -> None:
    first = subprocess.run([tool, "-C"], capture_output=True, text=True, env=_ENV).stdout.splitlines()[:1]
    found = re.match(r"PCRE2 version (\S+)", first[0]) if first else None
    if not found or found.group(1) != VERSION:
        sys.exit(f"this session needs PCRE2 {VERSION}, found: {first[0] if first else 'nothing'}")


def run(script: str) -> None:
    """Prints the pcre2test transcript of the script (patterns, subjects and results)."""
    tool = _tool()
    _check_version(tool)
    lines = script.strip("\n").splitlines()
    text = "\n".join(_dedent(lines)) + "\n"
    with tempfile.TemporaryDirectory() as folder:
        source = Path(folder) / "patterns.txt"
        source.write_text(text, encoding="utf-8")
        done = subprocess.run([tool, "-q", str(source)], capture_output=True, text=True, encoding="utf-8", env=_ENV)
    print(f"$ pcre2test -q patterns.txt   (PCRE2 {VERSION})")
    sys.stdout.write(done.stdout)
    if done.stderr:
        sys.stdout.write(done.stderr)
    if done.returncode != 0:
        print(f"[exit status {done.returncode}]")
    sys.stdout.flush()


def _dedent(lines: list[str]) -> list[str]:
    """Removes the indentation the whole script shares, so it can sit indented inside a Python call."""
    widths = [len(line) - len(line.lstrip(" ")) for line in lines if line.strip()]
    cut = min(widths) if widths else 0
    return [line[cut:] for line in lines]

The last pair is the one worth copying: a quoted field written as "[^"]*+" keeps every character of "yes" and still matches, because the class inside it can never reach the closing quote. The Python tab also checks the equivalence the documentation states, that x*+ means the same as the atomic group (?>x*), by asking both forms about aaa and comparing their answers.

Key idea

A possessive quantifier matches as much as a greedy one and then throws away every position it could have come back to, so whatever fails after it cannot be rescued by shortening the repetition.

Atomic groups lock up a whole group

A possessive quantifier needs a quantifier to attach to. When the part to lock up has none, PCRE2’s and Python’s answer is (?>...), which matches the text its own pattern matches first and then refuses to be re-entered.

The clearest case is a group of alternatives. (?:in|inch)es matches inches: the group takes in, the rest of the pattern fails on ch, the engine returns and tries inch, and the match succeeds. Write the same group as (?>in|inch)es and there is no match at all, because the group keeps in and the alternatives behind it are gone. Turn the alternatives round, (?>inch|in)es, and the match is back: inside an atomic group, the order of the alternatives decides the outcome.

An atomic group on alternatives, and around a repeated group

Python 3.14.8 · atomic/atomic.py

"""What an atomic group (?>...) locks up, in CPython 3.14.8 (atomic groups need 3.11 or later).

An atomic group holds whatever its own pattern matched first and never tries another way, so the order of the
alternatives inside it decides what the rest of the pattern sees.
"""

import re

CASES = [
    (r"(?:in|inch)es", "inches"),
    (r"(?>in|inch)es", "inches"),
    (r"(?>inch|in)es", "inches"),
    (r"(?:a|ab)c", "abc"),
    (r"(?>a|ab)c", "abc"),
    (r"(?>a|ab)bc", "abc"),
]


def found(pattern: str, subject: str) -> str:
    """The matched text as a quoted string, or 'no match'."""
    hit = re.search(pattern, subject)
    return repr(hit.group(0)) if hit else "no match"


for pattern, subject in CASES:
    print(f"{pattern:16} on {subject!r:10} -> {found(pattern, subject)}")

print()
print("One atomic group around a repeated group, on four id lists:")
weak = r"^(?:\d+,?)+$"
locked = r"^(?>(?:\d+,?)+)$"
for subject in ["1,2,3,4", "1234", "12,,3", "1,2,3x"]:
    answers = [bool(re.search(p, subject)) for p in (weak, locked)]
    print(f"  {subject!r:10} plain: {answers[0]!s:5} atomic: {answers[1]!s:5} same answer: {answers[0] == answers[1]}")

Output

(?:in|inch)es    on 'inches'   -> 'inches'
(?>in|inch)es    on 'inches'   -> no match
(?>inch|in)es    on 'inches'   -> 'inches'
(?:a|ab)c        on 'abc'      -> 'abc'
(?>a|ab)c        on 'abc'      -> no match
(?>a|ab)bc       on 'abc'      -> 'abc'

One atomic group around a repeated group, on four id lists:
  '1,2,3,4'  plain: True  atomic: True  same answer: True
  '1234'     plain: True  atomic: True  same answer: True
  '12,,3'    plain: False atomic: False same answer: True
  '1,2,3x'   plain: False atomic: False same answer: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 atomic.py

PCRE2 10.49 · atomic/atomic_pcre2.py

"""Atomic groups in PCRE2 10.49, through pcre2test, with the alphabetic form PCRE2 also accepts.

The pattern lines are run with no_auto_possess, so each one behaves exactly as written. (*atomic:...) is PCRE2's
longer spelling of (?>...); both lock up what the group matched first.
"""

from pcre2_session import run

run(r'''
/(?:in|inch)es/no_auto_possess
    inches

# the group keeps "in", and the rest of the pattern meets "ch"
/(?>in|inch)es/no_auto_possess
    inches

# the same group with its alternatives the other way round
/(?>inch|in)es/no_auto_possess
    inches

# the alphabetic spelling of the same construct
/(*atomic:inch|in)es/no_auto_possess
    inches

# an atomic group around a repeated group: the id list still matches
/^(?>(?:\d+,?)+)$/no_auto_possess
    1,2,3,4
    12,,3
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/(?:in|inch)es/no_auto_possess
    inches
 0: inches

# the group keeps "in", and the rest of the pattern meets "ch"
/(?>in|inch)es/no_auto_possess
    inches
No match

# the same group with its alternatives the other way round
/(?>inch|in)es/no_auto_possess
    inches
 0: inches

# the alphabetic spelling of the same construct
/(*atomic:inch|in)es/no_auto_possess
    inches
 0: inches

# an atomic group around a repeated group: the id list still matches
/^(?>(?:\d+,?)+)$/no_auto_possess
    1,2,3,4
 0: 1,2,3,4
    12,,3
No match

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 atomic_pcre2.py

The second half of each run wraps a whole repeated group, ^(?>(?:\d+,?)+)$, and asks it about four id lists. Every answer is the one the plain pattern gave, because the group covers everything between the two anchors: once it has matched as far as it can, there is nothing left for the rest of the pattern to want. PCRE2 accepts a longer spelling of the same construct, (*atomic:...), which the last-but-one block of its tab records.

Possessive form The atomic group it stands for
x*+ (?>x*)
x++ (?>x+)
x?+ (?>x?)
x{3,5}+ (?>x{3,5})
none: a group matched once (?>in|inch)

PCRE2’s documentation puts it plainly: the two forms carry no difference in meaning, though a possessive quantifier may run slightly faster. Read the table in the other direction too, as the last row does: an atomic group covers cases a possessive quantifier cannot reach.

Which of the two to write

Use the possessive form when one repeated item or one repeated group is the whole story, because the extra plus sign is hard to misread. Reach for (?>...) when the part to lock up is a group of alternatives, a sequence, or anything else without a quantifier of its own.

Checkpoint

Check: What a possessive quantifier keeps · Atomic groups and alternatives

  1. Question 1 of 2 In Python 3.14.8, what does re.search(r"a++a", "aaa") return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: None: a++ keeps all three characters and the final a has none left

    a++ matches all three a characters and throws away the positions where it could have stopped at one or two. The final a of the pattern then has nothing to match, and no saved position is left to return to, so the whole search fails.

  2. Question 2 of 2 The pattern (?>in|inch)es finds nothing in inches. Which single change makes it match?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Swap the alternatives: (?>inch|in)es

    An atomic group matches what its own pattern matches first and is never re-entered, so the order of its alternatives decides the outcome. The atomic example records no match for one order and a full match for the other.

Counting the work a lock saves

Here is a pattern somebody might write for a list of numeric ids with optional commas: ^(?:\d+,?)+$. It is a repeated group holding a repetition, and that shape is where the work explodes. Each digit can belong to the inner \d+ or start a new turn of the outer group, so the number of ways to split a line doubles with every digit, and a line that cannot match is only refused after every split has been tried.

PCRE2 counts that work itself. Its match limit caps the turns the engine takes round its matching loop, and pcre2test can search for the smallest limit under which an attempt still completes, so the printed number is the work the attempt needed rather than a time that would differ on every machine. The run below asks for that number on four lengths of a line of digits that ends in a character no id list allows:

What three forms of one pattern cost, counted by PCRE2 Python · limits/limits_pcre2.py
r"""How much work three forms of one pattern need, counted by PCRE2 10.49 itself.

The subject modifier find_limits_noheap makes pcre2test run the match again and again, lowering the match limit and
the depth limit until the attempt stops completing, and print the smallest values that still work. The match limit
counts the turns PCRE2 takes round its matching loop, so the printed number is the work the attempt needed: a
backtracking count, not a time.

The pattern is one somebody might write for a list of numeric ids with optional commas. \[1]{n} in a subject line is
pcre2test's way of writing n copies of the character 1, and the x at the end is the character that makes the list
invalid, so every form must answer "No match".
"""

from pcre2_session import run

run(r'''
# as written: a repeated group inside a repeated group
/^(?:\d+,?)+$/
    \[1]{8}x\=find_limits_noheap
    \[1]{12}x\=find_limits_noheap
    \[1]{16}x\=find_limits_noheap
    \[1]{20}x\=find_limits_noheap
    1,2,3,4\=find_limits_noheap

# both quantifiers made possessive
/^(?:\d++,?)++$/
    \[1]{8}x\=find_limits_noheap
    \[1]{20}x\=find_limits_noheap
    1,2,3,4\=find_limits_noheap

# the original pattern wrapped in one atomic group
/^(?>(?:\d+,?)+)$/
    \[1]{20}x\=find_limits_noheap
    1,2,3,4\=find_limits_noheap
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
# as written: a repeated group inside a repeated group
/^(?:\d+,?)+$/
    \[1]{8}x\=find_limits_noheap
Minimum match limit = 384
Minimum depth limit = 10
No match
    \[1]{12}x\=find_limits_noheap
Minimum match limit = 6144
Minimum depth limit = 14
No match
    \[1]{16}x\=find_limits_noheap
Minimum match limit = 98304
Minimum depth limit = 18
No match
    \[1]{20}x\=find_limits_noheap
Minimum match limit = 1572864
Minimum depth limit = 22
No match
    1,2,3,4\=find_limits_noheap
Minimum match limit = 9
Minimum depth limit = 9
 0: 1,2,3,4

# both quantifiers made possessive
/^(?:\d++,?)++$/
    \[1]{8}x\=find_limits_noheap
Minimum match limit = 4
Minimum depth limit = 3
No match
    \[1]{20}x\=find_limits_noheap
Minimum match limit = 4
Minimum depth limit = 3
No match
    1,2,3,4\=find_limits_noheap
Minimum match limit = 7
Minimum depth limit = 3
 0: 1,2,3,4

# the original pattern wrapped in one atomic group
/^(?>(?:\d+,?)+)$/
    \[1]{20}x\=find_limits_noheap
Minimum match limit = 5
Minimum depth limit = 5
No match
    1,2,3,4\=find_limits_noheap
Minimum match limit = 10
Minimum depth limit = 10
 0: 1,2,3,4

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 limits_pcre2.py

Characters that cannot match ^(?:\d+,?)+$ ^(?:\d++,?)++$
8 digits and a letter 384 4
12 digits and a letter 6,144 —
16 digits and a letter 98,304 —
20 digits and a letter 1,572,864 4

Every four digits multiply the plain pattern’s work by sixteen, while the possessive form stays at 4 turns whatever the length, because no split is ever tried twice. The depth limit, which measures how deep the engine’s own stack of saved positions goes, follows the same split: 22 against 3 at twenty digits. On a valid list, 1,2,3,4, the plain pattern needs 9 turns, the possessive one 7 and the version wrapped in a single atomic group 10, and all three match.

Key idea

A lock changes the work from “every way of splitting the line” to “one way”, which is why its effect grows with the length of the input: 384 turns against 4 at eight characters, and 1,572,864 against 4 at twenty.

PCRE2 possessifies some quantifiers by itself

Before you reach for a plus sign, it is worth knowing that one engine has already done part of the work. auto-possessification: A step PCRE2 takes while compiling a pattern: where the item after a quantifier cannot match what that quantifier repeats, it makes the quantifier possessive by itself. (*NO_AUTO_POSSESS) switches it off. is a step PCRE2 takes while compiling a pattern: where the item after a quantifier cannot possibly match what the quantifier repeats, it makes that quantifier possessive on its own, because coming back could not help.

\d+x is such a pattern, since x is not a digit. The run below puts it on forty copies of one digit, first as PCRE2 compiles it by default and then with that step switched off:

One pattern, with and without PCRE2's own possessification Python · auto-possess/auto_possess_pcre2.py
r"""PCRE2 10.49 making a quantifier possessive on its own, and the same pattern with that step switched off.

Both lines run /\d+x/ on forty copies of the digit 1, which cannot match. no_start_optimize is on in both, so the
first run is not skipped by the separate check for a required character, and the two numbers can be compared.
no_auto_possess switches off the step that rewrites \d+x as \d++x.

find_limits_noheap prints the smallest match limit and depth limit under which the attempt still completes, so the
match limit is the number of turns PCRE2 took round its matching loop.
"""

from pcre2_session import run

run(r'''
# PCRE2 decides for itself that \d+ need not give anything back, because x is not a digit
/\d+x/no_start_optimize
    \[1]{40}\=find_limits_noheap

# the same pattern with that decision switched off: one turn of the loop per digit given back
/\d+x/no_start_optimize,no_auto_possess
    \[1]{40}\=find_limits_noheap

# writing the possessive quantifier by hand gives what PCRE2 already did
/\d++x/no_start_optimize,no_auto_possess
    \[1]{40}\=find_limits_noheap

# a quantifier whose own characters can follow it is never possessified: \s+ may give a newline back to $
/\s+$/no_start_optimize
    \[ ]{40}x\=find_limits_noheap

/\s++$/no_start_optimize
    \[ ]{40}x\=find_limits_noheap
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
# PCRE2 decides for itself that \d+ need not give anything back, because x is not a digit
/\d+x/no_start_optimize
    \[1]{40}\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
No match

# the same pattern with that decision switched off: one turn of the loop per digit given back
/\d+x/no_start_optimize,no_auto_possess
    \[1]{40}\=find_limits_noheap
Minimum match limit = 41
Minimum depth limit = 3
No match

# writing the possessive quantifier by hand gives what PCRE2 already did
/\d++x/no_start_optimize,no_auto_possess
    \[1]{40}\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
No match

# a quantifier whose own characters can follow it is never possessified: \s+ may give a newline back to $
/\s+$/no_start_optimize
    \[ ]{40}x\=find_limits_noheap
Minimum match limit = 41
Minimum depth limit = 3
No match

/\s++$/no_start_optimize
    \[ ]{40}x\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
No match

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 auto_possess_pcre2.py

Two turns of the matching loop become 41 when the step is off: one turn for each digit handed back. Writing \d++x by hand gives exactly the two turns of the default, which is the point of the step. The last pair shows where it cannot help: \s+$ keeps its 41 turns, because $ may stand before a final newline and a newline is white space, so the repetition really might have to give one character back. Written as \s++$ the same pattern takes 2.

Only one engine in this lesson does it

Auto-possessification belongs to PCRE2, so PHP gets it too. The documentation of Python’s re, JavaScript and Java describes no such step, so a lock there has to be written by hand. PCRE2 lets a pattern switch the step off with (*NO_AUTO_POSSESS), which is useful when you want to see what your pattern costs without it.

Checkpoint

Check: The work a lock saves · PCRE2's own possessification

  1. Question 1 of 2 PCRE2 needed 384 turns of its matching loop for ^(?:\d+,?)+$ on 8 digits that cannot match, and 6,144 on 12. How many turns did the possessive form ^(?:\d++,?)++$ need on 20 digits?

    Type a number.

    A hint

    The limits run prints the same number for the possessive form at 8 digits and at 20.

    Show the answer: Question 1 of 2

    Answer: 4 turns

    The possessive form tries one split of the line and no other, so its cost does not grow with the length: 4 turns at 8 digits and 4 at 20, against 1,572,864 for the plain pattern at 20.

  2. Question 2 of 2 Why does PCRE2 make the quantifier in \d+x possessive while compiling it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Because x is not a digit, so handing a digit back could never let x match

    PCRE2 compares the quantified item with what follows it. Digits and x share no character, so backtracking into \d+ cannot help, and the compiled pattern behaves as \d++x: 2 turns of the matching loop instead of 41 on forty digits.

Rewriting a pattern without changing its answers

A lock is not a performance setting; it is a change to the pattern’s meaning that often happens to be faster. The honest way to apply one is to decide which lines must be accepted and which refused, then ask both patterns about every line. The run below does that with a function that reports each line where the two disagree:

A settings line and a quoted field, rewritten and checked Python · rewrite/rewrite.py
"""A check that compares a pattern with its possessive rewrite on a fixed list of lines, in CPython 3.14.8.

Rewriting a quantifier as possessive is safe only when the new pattern accepts and refuses exactly the same text.
The agree() function answers that with no timing at all: it asks both patterns about every line and reports where
they disagree. Lines a parser must accept and lines it must refuse are both in each list.
"""

import re

SETTINGS = ["rows=20;page=2;", "rows=20;page=2", "rows=20", "rows=;page=2", "rows 20;", ""]
QUOTED = ['"yes"', 'say "yes" twice', '"yes" and "no"', '"unterminated', 'no quotes at all']


def agree(before: str, after: str, lines: list[str]) -> bool:
    """Prints the two patterns, every line on which they differ, and whether they agreed everywhere."""
    print(f"  {before}  ->  {after}")
    same = True
    for line in lines:
        answers = (bool(re.search(before, line)), bool(re.search(after, line)))
        if answers[0] != answers[1]:
            same = False
            print(f"    differs on {line!r}: before {answers[0]}, after {answers[1]}")
    print(f"    same answers on all {len(lines)} lines: {same}")
    return same


print("A settings line, with both quantifiers made possessive:")
agree(r"^(?:\w+=\w+;?)+$", r"^(?:\w++=\w++;?)++$", SETTINGS)

print()
print("A quoted field, rewritten in two ways:")
agree(r'"[^"]*"', r'"[^"]*+"', QUOTED)
agree(r'".*"', r'".*+"', QUOTED)

print()
print("What the second rewrite does to one line:")
for pattern in [r'".*"', r'".*+"', r'"[^"]*+"']:
    hit = re.search(pattern, '"yes" and "no"')
    got = repr(hit.group(0)) if hit else "no match"
    print(f"  {pattern:12} on the line with two quoted words -> {got}")

Output

A settings line, with both quantifiers made possessive:
  ^(?:\w+=\w+;?)+$  ->  ^(?:\w++=\w++;?)++$
    same answers on all 6 lines: True

A quoted field, rewritten in two ways:
  "[^"]*"  ->  "[^"]*+"
    same answers on all 5 lines: True
  ".*"  ->  ".*+"
    differs on '"yes"': before True, after False
    differs on 'say "yes" twice': before True, after False
    differs on '"yes" and "no"': before True, after False
    same answers on all 5 lines: False

What the second rewrite does to one line:
  ".*"         on the line with two quoted words -> '"yes" and "no"'
  ".*+"        on the line with two quoted words -> no match
  "[^"]*+"     on the line with two quoted words -> '"yes"'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rewrite.py

The settings pattern ^(?:\w+=\w+;?)+$ and its possessive form agree on all six lines, so the rewrite is safe. The quoted field shows both outcomes side by side: "[^"]*" keeps its five answers when it becomes "[^"]*+", while ".*" rewritten as ".*+" loses three of them, since . happily matches a quote and the locked repetition runs to the end of the line and never gives the closing quote back.

Adding a plus sign to a dot repetition

Wrong: ".*+" for a quoted field, which matched nothing on all three lines that hold a quoted word. Right: "[^"]*+", where the class cannot match a quote, so keeping everything is exactly what the field needs. The rule behind both: a repetition may be locked when the characters after it are outside what it matches.

That rule is also why a check like this one belongs in your test file next to the pattern. It costs a list of lines, it fails on the version that changed its answers, and it passes on the version that only changed its cost.

Which flavours accept them, and what the others do

Both forms came from Perl’s family of engines, so a flavour: A particular regular-expression syntax and feature set, such as ECMAScript, Python re, PCRE2 or RE2; a pattern is written for one of them. More in “What a regular expression is and where it runs” built on a different model may not have them at all. The runs of this page cover JavaScript, Python, PHP, PCRE2 and Go; the Java, Ruby and Perl rows follow each flavour’s own documentation, since no program of theirs runs on this page.

Flavour x++, x*+, x?+ x{m,n}+ (?>...)
JavaScript (V8, QuickJS) refused refused refused
Python re 3.11 and later yes yes yes
PCRE2 10.49 yes yes yes, also (*atomic:...)
PHP 8.5.11 yes yes yes
Java yes yes yes, called an independent group
Ruby yes no yes
Perl yes yes yes
Go regexp and RE2 refused refused refused

Python’s two features arrived together in 3.11, so a pattern with either form stops an older interpreter at compile time. Ruby’s documentation states that its brace quantifiers have no possessive form, which leaves (?>x{3,5}) as the way to write one there. JavaScript refuses every form, and so does Go, which needs none: its search follows every way at once, so a repeated group inside a repeated group never makes it re-try a split, and its documentation promises a running time that grows in proportion to the length of the input.

What JavaScript says to each form JavaScript · rejects-js/rejects.mjs
// What JavaScript does with a possessive quantifier and an atomic group. Only the error's name is printed, because
// the wording of a message differs between engines; the name is the part the language fixes.
// Press Edit, add a pattern of your own to TRIED, and run it again.

const TRIED = ['a++b', 'a*+a', 'a?+a', 'a{3,5}+aa', '(?>a+)a', '(*atomic:a+)a'];

for (const source of TRIED) {
  let answer;
  try {
    new RegExp(source);
    answer = 'compiled';
  } catch (error) {
    answer = error.name;
  }
  console.log(`new RegExp(${JSON.stringify(source)}) -> ${answer}`);
}

// The forms that do compile: a plain quantifier, a plain group, and the lookahead with a backreference.
console.log('');
for (const source of ['a+a', '(?:a|ab)c', '(?=(a+))\\1a']) {
  let answer;
  try {
    new RegExp(source);
    answer = 'compiled';
  } catch (error) {
    answer = error.name;
  }
  console.log(`new RegExp(${JSON.stringify(source)}) -> ${answer}`);
}

Output

new RegExp("a++b") -> SyntaxError
new RegExp("a*+a") -> SyntaxError
new RegExp("a?+a") -> SyntaxError
new RegExp("a{3,5}+aa") -> SyntaxError
new RegExp("(?>a+)a") -> SyntaxError
new RegExp("(*atomic:a+)a") -> SyntaxError

new RegExp("a+a") -> compiled
new RegExp("(?:a|ab)c") -> compiled
new RegExp("(?=(a+))\\1a") -> compiled

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node rejects.mjs

What Go says to each form, and the list it matches anyway Go · reject-go/rejects.go
// Go's regexp package follows RE2 syntax, which has neither possessive quantifiers nor atomic groups. Each pattern
// is compiled and the error, if any, is printed. The last lines run the pattern RE2 does accept.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func main() {
	for _, pattern := range []string{`a++b`, `a*+a`, `a{3,5}+aa`, `(?>a+)a`, `(?:a|ab)c`} {
		if _, err := regexp.Compile(pattern); err != nil {
			fmt.Printf("%-12s -> %v\n", pattern, err)
		} else {
			fmt.Printf("%-12s -> compiled\n", pattern)
		}
	}

	// RE2 needs none of them: it walks every way at once, so a repeated group inside a repeated group is safe.
	ids := regexp.MustCompile(`^(?:\d+,?)+$`)
	for _, subject := range []string{"1,2,3,4", "12,,3", strings.Repeat("1", 24) + "x"} {
		fmt.Printf("%-28q -> %t\n", subject, ids.MatchString(subject))
	}
}

Output

a++b         -> error parsing regexp: invalid nested repetition operator: `++`
a*+a         -> error parsing regexp: invalid nested repetition operator: `*+`
a{3,5}+aa    -> error parsing regexp: invalid nested repetition operator: `{3,5}+`
(?>a+)a      -> error parsing regexp: invalid or unsupported Perl syntax: `(?>`
(?:a|ab)c    -> compiled
"1,2,3,4"                    -> true
"12,,3"                      -> false
"111111111111111111111111x"  -> false

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run rejects.go

The JavaScript run prints SyntaxError for all six spellings, in V8 and in its browser twin alike; the error’s name is printed rather than its message, because the wording differs between engines. Go names the construct it refused: a nested repetition operator for the possessive forms, unsupported Perl syntax for (?>. Its last three lines put the plain id pattern to work, and the 25-character line that cannot match is refused at once with no lock at all.

Which way to lock a repetition up depends on the engine and on the pattern. Answer the three questions below and watch the options that do not fit drop out:

Interactive · answer and compare Which way to lock a repetition up Answer three questions about the pattern you are fixing, and watch which of the five ways are ruled out.
Which engine runs this pattern?
What has to stop being re-tried?
Is there a character that cannot appear inside the repeated part, such as a closing quote?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to stop an engine re-trying a repetition, against what each one asks of the pattern.
Way to write it Engines that accept it Keeps the same answers Covers a whole group Reads at a glance
A possessive quantifier What it locks: the item its quantifier repeats Mixed Python 3.11 and later, PCRE2, PHP, Java, Ruby and Perl; Ruby has no brace form Mixed may change them: a*+a stops matching aaa, so compare both on a list of lines Mixed only a group that carries a quantifier, such as the repeated group of an id list Good one extra + on the quantifier, which the engines above all read the same way
An atomic group What it locks: everything inside the group Mixed the same engines; PCRE2 and Perl also accept the longer spelling (*atomic:...) Mixed may change them: (?>in|inch)es stops matching inches, so compare both Good any part of the pattern, alternatives and nested groups included Mixed the (?>...) spelling is easy to miss and easy to confuse with a lookahead
A lookahead and a backreference What it locks: whatever the lookahead captured Mixed every engine with lookahead and backreferences, JavaScript among them Mixed the same risk as the other two, plus a capture group the rest of the pattern must skip Good whatever stands inside the lookahead, so a whole repeated group fits Poor three constructs working together; a comment beside the pattern is worth writing
A more precise pattern What it locks: nothing: there is less to re-try Good every engine: a negated class or an exact class is plain syntax Good you choose them: the quoted-field class took the same five answers as before Mixed it removes the choices instead of locking them, which needs a rethink of the pattern Good the pattern says what it accepts, which is what the next reader needs
An engine that re-tries nothing What it locks: nothing: it walks every way at once Poor Go regexp, RE2 and re2js only, and they have no backreferences or lookaround Good nothing in the pattern changes, so nothing about its answers changes Good the whole pattern: 24 digits that cannot match came back at once Good no guard to read, though the pattern must stay inside the RE2 syntax
How the answers decide
  • “Which engine runs this pattern?” JavaScript, in a browser or in Node.js: rules out A possessive quantifier, An atomic group and An engine that re-tries nothing. JavaScript compiles neither form: a++b and (?>a+)a both raise a SyntaxError, in V8 and in QuickJS alike.
  • “Which engine runs this pattern?” Go regexp, or another RE2 engine: rules out A possessive quantifier, An atomic group and A lookahead and a backreference. Go regexp refuses both forms and has no backreferences, and it needs no guard: it re-tries nothing.
  • “Which engine runs this pattern?” Two or more of these, with one pattern: rules out An engine that re-tries nothing and adds a caveat to A possessive quantifier and An atomic group. One pattern for several engines can only use what all of them read; the RE2 engines are not an option for the others.
  • “Which engine runs this pattern?” Python 3.11+, PCRE2, PHP, Java, Ruby or Perl: adds a caveat to A lookahead and a backreference. These engines all have lookahead and backreferences, but the two direct forms say the same thing in far less space.
  • “What has to stop being re-tried?” A group of alternatives, matched once: rules out A possessive quantifier. A possessive quantifier needs a quantifier to attach to, and a group matched once has none: (?>in|inch) does.
  • “Is there a character that cannot appear inside the repeated part, such as a closing quote?” Yes, the part has a clear end: adds a caveat to An engine that re-tries nothing. A class that cannot reach past the end character, such as [^"]*+, is the smallest change with the fewest surprises.
  • “Is there a character that cannot appear inside the repeated part, such as a closing quote?” No, anything may appear in it: adds a caveat to A possessive quantifier, An atomic group and A lookahead and a backreference. With nothing ruled out of the repeated part, a lock may refuse text the old pattern accepted: compare the answers first.

In short: write the possessive form where the engine accepts it and one quantifier covers the part to lock, use (?>...) for anything larger, and write a more precise pattern whenever the repetition has a clear end character, because that keeps the answers as well as the cost.

Checkpoint

Check: Which flavours accept them

  1. Question 1 of 2 Ruby accepts a++ and (?>a+) but not one of the possessive forms. Which one, and what do you write instead?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The brace form a{3,5}+; write the atomic group (?>a{3,5})

    Every flavour of this lesson that has possessive quantifiers accepts the star, plus and question-mark forms. Ruby is the one whose counted form is missing, and an atomic group around a{3,5} says the same thing there.

  2. Question 2 of 2 Why does Go's regexp package need neither a possessive quantifier nor an atomic group?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It follows every way of matching at once, so it never re-tries a split of the line

    Go follows RE2 syntax, whose search walks all the ways at the same time instead of trying one and coming back. The constructs of this lesson exist to throw away saved positions, and that engine saves none.

JavaScript has no possessive form, but its lookahead is atomic

The missing syntax does not mean the effect is out of reach. In ECMA-262 a lookahead settles on the first way its inner pattern fits where it stands, and the engine never returns into a finished lookahead to look for another fit. A lookahead that captures what it matched, followed by a backreference to that capture, therefore behaves like an atomic group: the lookahead finds its one answer, and the backreference matches the same text for real.

The shape is (?=(X))\1 in place of (?>X). The run below puts it beside the plain forms on the subjects of this lesson, then on a list of ids:

The lookahead and backreference that lock a repetition up JavaScript · atomic-js/atomic.mjs
// JavaScript's own way to lock a repetition up: a lookahead that captures what it matched, and a backreference
// that matches the same text again. The lookahead is never re-entered, so the capture cannot be adjusted later.
// Press Edit, put your own list in SUBJECTS, and run it again.

const PAIRS = [
  ['plain repetition', /^(a+)a$/, 'aaa'],
  ['locked repetition', /^(?=(a+))\1a$/, 'aaa'],
  ['plain group', /^(?:a|ab)c$/, 'abc'],
  ['locked group', /^(?=(a|ab))\1c$/, 'abc'],
];

for (const [label, pattern, subject] of PAIRS) {
  console.log(`${label.padEnd(18)} ${String(pattern).padEnd(20)} on ${JSON.stringify(subject)} -> ${pattern.test(subject)}`);
}

// The same answers on a list of ids, with the repetition locked up.
const plain = /^(?:\d+,?)+$/;
const locked = /^(?=((?:\d+,?)+))\1$/;
const SUBJECTS = ['1,2,3,4', '1234', '1,2,', '12,,3', '1,2,3x', ''];

console.log('');
let agree = true;
for (const subject of SUBJECTS) {
  const a = plain.test(subject);
  const b = locked.test(subject);
  agree = agree && a === b;
  console.log(`  ${JSON.stringify(subject).padEnd(10)} plain: ${String(a).padEnd(6)} locked: ${String(b).padEnd(6)} same answer: ${a === b}`);
}
console.log(`same answer on all ${SUBJECTS.length} subjects: ${agree}`);

// One id list of 24 digits that no comma can save, timed coarsely: the numbers of a single run are left out, because
// they differ on every machine, and only the two thresholds are printed.
const bad = `${'1'.repeat(24)}x`;
let started = Date.now();
plain.test(bad);
const plainMs = Date.now() - started;
started = Date.now();
locked.test(bad);
const lockedMs = Date.now() - started;
console.log('');
console.log(`on ${bad.length} characters that cannot match: the plain pattern took over 20 ms: ${plainMs > 20}`);
console.log(`the locked pattern took under 10 ms: ${lockedMs < 10}`);

Output

plain repetition   /^(a+)a$/            on "aaa" -> true
locked repetition  /^(?=(a+))\1a$/      on "aaa" -> false
plain group        /^(?:a|ab)c$/        on "abc" -> true
locked group       /^(?=(a|ab))\1c$/    on "abc" -> false

  "1,2,3,4"  plain: true   locked: true   same answer: true
  "1234"     plain: true   locked: true   same answer: true
  "1,2,"     plain: true   locked: true   same answer: true
  "12,,3"    plain: false  locked: false  same answer: true
  "1,2,3x"   plain: false  locked: false  same answer: true
  ""         plain: false  locked: false  same answer: true
same answer on all 6 subjects: true

on 25 characters that cannot match: the plain pattern took over 20 ms: true
the locked pattern took under 10 ms: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node atomic.mjs

/^(a+)a$/ matches aaa and /^(?=(a+))\1a$/ does not, exactly as a++a does not in Python; /^(?=(a|ab))\1c$/ refuses abc just as (?>a|ab)c does. On six id lists the locked pattern gives every answer the plain one gave, and on a line of 24 digits that cannot match it is the plain pattern that takes over 20 milliseconds while the locked one stays under 10, in both engines. Press Edit, put your own lines in SUBJECTS, and run it again.

Forgetting that the lookahead leaves a capture behind

Wrong: adding (?=(X))\1 to a pattern whose later groups are counted by number, which silently shifts every one of them by one. Right: write the lock first and renumber, or give the groups names, which the lesson on groups covers. The same goes for a replacement string that refers to a group: A part of a pattern wrapped in parentheses whose matched text the engine remembers separately, for reuse or extraction. More in “What a regular expression is and where it runs” by number.

TC39’s proposal-regexp-atomic-operators repository proposes atomic operators for JavaScript, but the language itself has no such syntax, so a pattern written today needs the lookahead. Since the trick rests on a backreference, it is unavailable in RE2 engines, which have neither backreferences nor lookahead.

Checkpoint

Check: The JavaScript lookahead lock

  1. Question 1 of 2 Which JavaScript pattern behaves like the atomic group (?>\d+) followed by a comma?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: /(?=(\d+))\1,/

    ECMA-262 says only the first way a lookahead can match is tried, and that there is no backtracking into it, so the capture it leaves cannot be adjusted later. Matching that capture again with \1 consumes the text for real.

  2. Question 2 of 2 What does the lookahead-and-backreference lock add to a pattern, besides the lock?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A capture group, so every group numbered after it shifts by one

    (?=(X))\1 needs the capture, so numbered groups after it move by one and a replacement string that names a group by number has to be renumbered as well. Named groups avoid the problem.

Practise: three patterns to lock up

The exercise gives you three patterns from a parser that reads its own log lines, each with a list of lines it must accept and a list it must refuse. Rewrite each one so that every answer stays the same and no split is ever tried twice: with possessive quantifiers or an atomic group in Python, and with the lookahead and backreference in JavaScript. The sample tests check both the answers and the construct.

Exercise · Medium · JavaScript, Python

Lock up three patterns of a log reader

A log reader checks three kinds of line with three patterns. All three work, and all three try every way of splitting a line before they refuse one, so a long line that cannot match costs far more than it should.

Rewrite the three patterns in PATTERNS so that each one locks its repetition up and still gives exactly the same answer on every line of the tests:

  • ids: a list of ids, digits with optional commas, as in 12,34 or 1,2,. The pattern is ^(?:\d+,?)+$.
  • settings: name=value pairs with optional semicolons, as in rows=20;page=2. The pattern is ^(?:\w+=\w+;?)+$.
  • quoted: one quoted field that may hold a backslash escape, as in "say \"yes\"". The pattern is ^"(?:[^"\\]|\\.)*"$.

In Python, use possessive quantifiers (*+, ++, ?+) or atomic groups ((?>...)). In JavaScript, where both are refused, use a lookahead that captures what it matched and a backreference to that capture: (?=(X))\1 in place of (?>X).

The tests check two things for each pattern: that it accepts and refuses exactly the lines it did before, and that it carries a lock at all. Watch out for a rewrite that changes an answer, the way ".*+" stops matching any quoted field: accepts must return the same value as the old pattern on each of the listed lines.

JavaScript · Starter code · patterns.mjs

// Three patterns of a log reader. Each one works and each one tries every way of splitting a line before it refuses
// one. Rewrite all three so that they keep their answers and lock their repetition up.
//
// JavaScript refuses a++ and (?>...), so the lock is a lookahead that captures what it matched, followed by a
// backreference to that capture: (?=(X))\1 in place of (?>X).

export const PATTERNS = {
  // a list of ids: digits with optional commas
  ids: '^(?:\\d+,?)+$',
  // name=value pairs with optional semicolons
  settings: '^(?:\\w+=\\w+;?)+$',
  // one quoted field that may hold a backslash escape
  quoted: '^"(?:[^"\\\\]|\\\\.)*"$',
};

/** Whether the named pattern accepts the line. */
export function accepts(name, line) {
  return new RegExp(PATTERNS[name]).test(line);
}
The sample tests · patterns.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { PATTERNS, accepts } from './patterns.mjs';

const LINES = {
  ids: {
    accept: ['1', '12,34', '1,2,3', '1,2,', '007,8'],
    refuse: ['', '1,,2', '1,2,x', 'a,1', ',1'],
  },
  settings: {
    accept: ['rows=20', 'rows=20;', 'rows=20;page=2', 'a=1;b=2;'],
    refuse: ['', 'rows=;page=2', 'rows 20;', '=20;', 'rows=20;;'],
  },
  quoted: {
    accept: ['""', '"yes"', '"say \\"yes\\""', '"a;b=c"'],
    refuse: ['', 'yes', '"unterminated', '"yes" twice', '"yes"x'],
  },
};

// A lock in JavaScript: a lookahead that captures, and a backreference to that capture.
const LOCKED = /\(\?=\([\s\S]*\)\)[\s\S]*\\[1-9]/;

for (const [name, lines] of Object.entries(LINES)) {
  test(`${name}: every line that was accepted is still accepted`, () => {
    for (const line of lines.accept) {
      assert.equal(accepts(name, line), true, `${name} should accept ${JSON.stringify(line)}`);
    }
  });

  test(`${name}: every line that was refused is still refused`, () => {
    for (const line of lines.refuse) {
      assert.equal(accepts(name, line), false, `${name} should refuse ${JSON.stringify(line)}`);
    }
  });

  test(`${name}: the repetition is locked up`, () => {
    const source = PATTERNS[name];
    assert.ok(typeof source === 'string' && source.length > 0, `PATTERNS.${name} should be a pattern string`);
    assert.ok(
      LOCKED.test(source),
      `${name} still has nothing to stop the engine re-trying a split: use (?=(X))\\1 around the repetition`,
    );
  });
}

test('each pattern still compiles on its own', () => {
  for (const name of Object.keys(LINES)) {
    const built = new RegExp(PATTERNS[name]);
    assert.ok(built instanceof RegExp, `${name} should compile`);
  }
});

Python · Starter code · patterns.py

"""Three patterns of a log reader. Each one works and each one tries every way of splitting a line before it refuses
one. Rewrite all three so that they keep their answers and lock their repetition up.

Python 3.11 and later accept possessive quantifiers (*+, ++, ?+) and atomic groups ((?>...)); either form counts as
a lock here.
"""

import re

PATTERNS = {
    # a list of ids: digits with optional commas
    "ids": r"^(?:\d+,?)+$",
    # name=value pairs with optional semicolons
    "settings": r"^(?:\w+=\w+;?)+$",
    # one quoted field that may hold a backslash escape
    "quoted": r'^"(?:[^"\\]|\\.)*"$',
}


def accepts(name: str, line: str) -> bool:
    """Whether the named pattern accepts the line."""
    return re.search(PATTERNS[name], line) is not None
The sample tests · test_patterns.py
"""Sample tests for the three patterns: the same answers as before, and a lock in each pattern."""

import re

from patterns import PATTERNS, accepts

LINES = {
    "ids": {
        "accept": ["1", "12,34", "1,2,3", "1,2,", "007,8"],
        "refuse": ["", "1,,2", "1,2,x", "a,1", ",1"],
    },
    "settings": {
        "accept": ["rows=20", "rows=20;", "rows=20;page=2", "a=1;b=2;"],
        "refuse": ["", "rows=;page=2", "rows 20;", "=20;", "rows=20;;"],
    },
    "quoted": {
        "accept": ['""', '"yes"', '"say \\"yes\\""', '"a;b=c"'],
        "refuse": ["", "yes", '"unterminated', '"yes" twice', '"yes"x'],
    },
}

# A lock in Python: a possessive quantifier, or an atomic group.
LOCKED = re.compile(r"(?:\*\+|\+\+|\?\+|\}\+|\(\?>)")


def test_accepted_lines_are_still_accepted():
    for name, lines in LINES.items():
        for line in lines["accept"]:
            assert accepts(name, line) is True, f"{name} should accept {line!r}"


def test_refused_lines_are_still_refused():
    for name, lines in LINES.items():
        for line in lines["refuse"]:
            assert accepts(name, line) is False, f"{name} should refuse {line!r}"


def test_every_pattern_carries_a_lock():
    for name in LINES:
        source = PATTERNS[name]
        assert isinstance(source, str) and source, f"PATTERNS[{name!r}] should be a pattern string"
        assert LOCKED.search(source), (
            f"{name} still has nothing to stop the engine re-trying a split: "
            "use a possessive quantifier or an atomic group"
        )


def test_every_pattern_still_compiles():
    for name in LINES:
        assert re.compile(PATTERNS[name]) is not None, f"{name} should compile"
A hint

Two of the three patterns repeat a group, so the lock has to cover the inner repetition as well as the outer one: (?:\d+,?)+ has a plus sign in two places. In Python each of them takes its own extra plus sign; in JavaScript one lookahead around the whole repeated group is enough, because nothing inside it is re-tried once the lookahead has answered.

Before you change the third pattern, ask what its repeated part can match: its first alternative refuses a quote, and its second one is a backslash followed by anything, so the closing quote of the pattern can never be swallowed. That is what makes the lock safe there, and it is also the reason a lock on a plain .* would not be.

The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Regex Tester Paste a pattern and a line to see what your browser's own engine matches. It is the JavaScript flavour, so the possessive forms raise an error there and the lookahead version works.

Now put the seven ideas to the test. The check below picks the one you handle least well and asks about that first, with a word on every choice the moment you pick it:

Practice that adapts to you

Practise this lesson

16 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 Why does a++b still match aaab although the quantifier gives nothing back?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: Because b follows the three a characters, so nothing has to be given back

    a++ takes aaa and keeps it. The pattern then wants b, which is the next character, so the match succeeds with no need for any saved position. The same pattern on aaa alone has no match at all.

  2. Question 2 of 16 A quoted field is matched with "[^"]*+". On the line say "yes" twice, what does it return?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: The text "yes", quotes included

    The negated class [^"] is the reason the lock is safe here: the characters the repetition can take and the character after it do not overlap, so the possessive form matches exactly what the plain form matched.

  3. Question 3 of 16 On six a characters, a{3,5}aa matches but a{3,5}+aa does not. What is the engine doing differently?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: The plain form tries five a characters, then four, and the four-character run leaves two for aa

    a{3,5} may take 5, 4 or 3 characters. Greedy order tries 5 first, fails on aa with one character left, and then tries 4, which works. a{3,5}+ takes 5 and discards the other two lengths, so the pattern fails.

  4. Question 4 of 16 Which of these pairs give the same answers on every subject?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: a++ and (?>a+)

    The documented equivalence is between a possessive quantifier and the atomic group of the same quantified item. Wrapping a group of alternatives changes the pattern''s answers, which is the point of the inches case.

  5. Question 5 of 16 When can a part of a pattern be locked only with (?>...) and not with a possessive quantifier?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: When it has no quantifier of its own, such as a group of alternatives matched once

    The possessive form needs a quantifier. For a sequence or a set of alternatives matched once, the atomic group is the only way to say the same thing, which is what (?>in|inch) does.

  6. Question 6 of 16 The limits run prints a minimum match limit for each attempt. What does that number count?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: The turns PCRE2 took round its matching loop before it could answer

    PCRE2 counts each turn of its main matching loop and refuses to go past the match limit. pcre2test lowers that limit until the attempt stops completing, so the printed value is a count of work, not a duration.

  7. Question 7 of 16 On the valid list 1,2,3,4, the plain pattern needed 9 turns, the possessive form 7, and the version wrapped in one atomic group 10. How many of the three matched the list?

    Type a number.

    A hint

    A lock changes the cost of an attempt; whether it changes the answer depends on the pattern.

    Show the answer: Question 7 of 16

    Answer: 3 patterns

    All three matched. The locks in this case remove only splits that could never lead to a match, so the answers are identical and the costs differ. The same three patterns refuse 12,,3 as well.

  8. Question 8 of 16 Why can PCRE2 not possessify \s+$ by itself, when it does possessify \d+x?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: Because $ can stand before a final newline, and a newline is white space

    Auto-possessification needs the item after the quantifier to be unable to match what the quantifier repeats. A newline is matched by \s and may be the character $ stands before, so the overlap stops the step: the run records 41 turns for \s+$ and 2 for \s++$.

  9. Question 9 of 16 Which engines of this lesson possessify simple quantifiers while compiling a pattern?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: PCRE2, and PHP through it

    Auto-possessification is a PCRE2 optimisation, documented in pcre2pattern and switchable with (*NO_AUTO_POSSESS). In Python, Java or JavaScript the lock has to be written into the pattern.

  10. Question 10 of 16 Why does ".*+" match nothing on a line that holds a quoted word?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: Because . matches a quote too, so the locked repetition runs to the end of the line and keeps it

    The rewrite run shows ".*" matching and ".*+" not matching on three of its five lines. The repeated item and the character after it overlap, so this rewrite changes what the pattern accepts.

  11. Question 11 of 16 You have replaced a pattern with a locked version. Which check tells you, without any timing, that the replacement is sound?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: Ask both patterns about a list of lines that must be accepted and lines that must be refused, and compare every answer

    The rewrite example is that check: it takes the two patterns and a fixed list of lines and prints every line where they disagree. It passes for the settings pattern and for the quoted field with a negated class, and it fails for the dot version.

  12. Question 12 of 16 From which Python version do a++ and (?>a+) compile in the re module?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: 3.11

    Python's documentation marks both the possessive quantifiers and atomic grouping as added in version 3.11, so a pattern that uses them is a version requirement as well as a performance decision.

  13. Question 13 of 16 A pattern uses [0-9]{4,6}+ and must run in Ruby and in Python. What is the safe way to write it?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: Use the atomic group (?>[0-9]{4,6}), which both accept

    The counted possessive form is the one difference between these two flavours in this lesson. An atomic group around the counted quantifier is the portable spelling, and Python documents the two as equivalent.

  14. Question 14 of 16 What does new RegExp("(?>a+)b") do in Node.js and in a browser?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: It throws a SyntaxError

    Every possessive and atomic spelling of this lesson raises a SyntaxError in JavaScript, including the alphabetic (*atomic:...) form that PCRE2 accepts.

  15. Question 15 of 16 Why does (?=(X))\1 give the same effect as an atomic group, rather than just repeating the work?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: Only the first way the lookahead can match is tried, and the engine never backtracks into it, so its capture is fixed

    The specification notes that a lookahead tries only its first match and that there is no backtracking into it. So the capture stays as it was found, and \1 walks over exactly that text, which is what an atomic group does in one step.

  16. Question 16 of 16 Why can the lookahead lock not be used in Go's regexp package?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: RE2 syntax has neither lookahead nor backreferences

    The lookahead lock rests on two features RE2 leaves out by design, because they are what make a backtracking search necessary. An RE2 engine needs no lock, since it never re-tries a split.

Interview questions

Warm-up: what does the second plus sign in \d++ do, and when does it change what a pattern matches?

It makes the repetition final. \d+ matches as many digits as it can and remembers every shorter run it could fall back to; \d++ matches the same digits and remembers nothing, so if the rest of the pattern fails, the engine gives up at that starting point instead of handing digits back. The answer changes whenever the characters after the repetition are ones the repetition itself could have matched: a*+a never matches aaa, because a*+ keeps all three. When the next part of the pattern is outside what the repetition matches, as in \d++x or "[^"]*+", the accepted text is identical and only the work changes. x++ is a shorter spelling of the atomic group (?>x+), and Python has had both since 3.11.

Two questions go further, one on a pattern that has to be made cheap without changing what it accepts, and one on the same fix in a browser. Settle on your own opening move for each, and see where its page begins:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A Python 3.11 search service builds a pattern from a stored template and a term an operator types, and a few queries a day take minutes while the rest answer at once. A colleague proposes adding possessive quantifiers everywhere. How do you decide what to change, and how do you show that the service still answers the same?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A PHP service validates a reference code with a pattern that uses ++ and (?>...), and the same rule must now be checked in the browser before the form is sent. The pattern also captures two groups the page reads. How do you carry it over, and how do you keep the two sides agreeing?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does the second plus sign in \d++ change?

    It makes the repetition final: \d++ takes the same digits as \d+ but keeps no shorter run to fall back to, so if the rest of the pattern fails, the engine gives up at that start instead of handing digits back.

  2. Why does a*+a find nothing in aaa, while a+a matches all three characters?

    a*+ keeps all three a's and gives none back, so the last a in the pattern has nothing left to match. The plain repetition gives one a back.

  3. Why does (?>in|inch)es refuse inches while (?>inch|in)es accepts it?

    An atomic group throws away its saved positions once it matches. In the first, in matches, es then fails on ch, and inch is never tried; in the second, inch matches first and es follows.

  4. How much work did two possessive plus signs save on the list pattern ^(?:\d+,?)+$?

    To refuse twenty digits followed by a stray character, PCRE2 needed 1,572,864 turns of its matching loop for the plain pattern and 4 for the possessive one, and both accepted every valid list.

  5. When can a repetition be locked without changing what the pattern accepts?

    When the characters after it are outside what it matches. "[^"]*+" keeps its answers, while ".*+" matches no quoted field, because the dot also takes the closing quote. Confirm the rewrite on a list of lines.

  6. Which quantifier does PCRE2 make possessive by itself, and which can it not?

    It possessifies \d+ in \d+x, because x is not a digit, cutting the cost from 41 turns to 2. It cannot do so for \s+$, because a newline is white space.

  7. JavaScript has no possessive quantifier. How do you get the same lock there?

    With a lookahead and a backreference, (?=(X))\1. It adds a capture group, which shifts every later numbered group by one. Go needs no lock, because it never re-tries a path.

Key takeaways

  • A possessive quantifier matches as much as a greedy one and then throws away every position the engine could come back to, so a++a and a*+a find nothing in aaa while a+a matches all three characters.
  • An atomic group (?>...) does the same for anything inside it, which is how a group of alternatives is locked: (?>in|inch)es refuses inches, and (?>inch|in)es accepts it.
  • The work saved grows with the input: PCRE2 needed 1,572,864 turns of its matching loop to refuse twenty digits with the plain pattern and 4 with the possessive one, and both matched every valid list.
  • PCRE2 makes some quantifiers possessive while compiling, so \d+x already costs 2 turns instead of 41, but \s+$ cannot be treated that way because a newline is white space.
  • A rewrite is safe only when both patterns answer the same way on a list of lines you choose: "[^"]*+" keeps its answers and ".*+" loses three of them.
  • JavaScript and Go refuse both forms; in JavaScript the lookahead and backreference (?=(X))\1 give the same effect, while Go needs no lock because it re-tries nothing.

Terms in this lesson

atomic group
A group written (?>...) that keeps the first match its own pattern finds: a failure later in the pattern cannot backtrack into it to make it try another way. x++ means the same as (?>x+).
auto-possessification
A step PCRE2 takes while compiling a pattern: where the item after a quantifier cannot match what that quantifier repeats, it makes the quantifier possessive by itself. (*NO_AUTO_POSSESS) switches it off.
possessive quantifier
A quantifier with an extra plus sign, such as x*+, x++, x?+ or x{m,n}+, that matches as much as a greedy one and then keeps it: it saves no shorter run, so a failure later in the pattern never makes it give characters back.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.