proj/portage:master commit in: lib/portage/tests/dep/, lib/portage/dep/

"Matt Turner" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1785963929.34257307e1fe295885f6851a596cc17b53cee705.mattst88@gentoo>
commit:     34257307e1fe295885f6851a596cc17b53cee705
Author:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug  2 20:22:10 2026 +0000
Commit:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug  5 21:05:29 2026 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=34257307

dep: widen the Atom C fast path to wildcard-flag and is_valid_flag callers

Instrumenting a profiled @world showed the Atom fast path was reaching
only 37% of the 779651 Atom.__init__ calls, with the guard rejecting
376131 of them (48%) before scan_atom was even tried.  The two terms
responsible were unnecessary:

  - allow_wildcard: __init__ always tries the strict regex first and
    only uses the wildcard regex when it fails.  scan_atom accepts
    exactly the strict-valid atoms, so a real wildcard/extended atom
    fails scan_atom and falls back on its own; a non-wildcard atom
    parses identically whether or not allow_wildcard is set.
    (allow_wildcard blocked only 6556 calls anyway.)

  - is_valid_flag: this only drives a USE-conditional-flag-in-IUSE
    check, which the fast path can perform itself.  It was the real
    bottleneck, blocking 369575 calls (47%).

Drop both from the guard and run the is_valid_flag conditional-flag
validation inside _c_fast_init, mirroring the regex path exactly (same
IUSE.missing InvalidAtom).  The fast path now serves 663538 of 779655
Atom.__init__ calls (85%) -- nearly every call not excluded by an
injected _use or a virtual-expansion original.

Two differential fuzzes (~1.3M and ~1.25M comparisons) covering wildcard
atoms and is_valid_flag callbacks across EAPIs {5,8,None} found no
differences in any Atom field, str(), without_use, or raised exception.

Profiled cProfile emerge -uDNp @world: Atom.__init__ 10.68s -> 6.98s cum
(_c_fast_init 288281 -> 663538 calls); profiled resolution 145.7s ->
143.3s.  Unprofiled, the whole series takes emerge -uDNp @world from
54.3s on the pure-Python path to 50.1s (best of three).  @world merge
list unchanged.

Extends TestScanAtom with cases for the is_valid_flag validation
(accepting and rejecting flags, and that non-conditional flags are not
validated) and for extended/wildcard atoms (*/*, cat/*, =cat/pkg-*1*),
which fail scan_atom and fall back to the regex path.

Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>

 lib/portage/dep/__init__.py            | 36 ++++++++++++++++++++++++----------
 lib/portage/tests/dep/test_c_parser.py | 21 ++++++++++++++++++++
 2 files changed, 47 insertions(+), 10 deletions(-)

diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index 1556c1a62..dac8b1987 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -1826,7 +1826,23 @@ class Atom:
         def __init__(self, forbid_overlap=False):
             self.overlap = self._overlap(forbid=forbid_overlap)
 
-    def _c_fast_init(self, catom, eapi, eapi_attrs, unevaluated_atom):
+    def _validate_conditional_flags(self, is_valid_flag):
+        """Raise if a USE-conditional flag in this atom is not in IUSE.
+
+        Mirrors the check the regex path performs in __init__.
+        """
+        for conditional_type, flags in self._use.conditional.items():
+            for flag in flags:
+                if is_valid_flag(flag):
+                    continue
+                conditional_str = _use_dep._conditional_strings[conditional_type]
+                raise InvalidAtom(
+                    f"USE flag '{flag}' referenced in conditional "
+                    f"'{conditional_str % flag}' in atom '{self}' is not in IUSE",
+                    category="IUSE.missing",
+                )
+
+    def _c_fast_init(self, catom, eapi, eapi_attrs, is_valid_flag, unevaluated_atom):
         """Populate this Atom from a C scan_atom result.
 
         Raises InvalidAtom/TypeError for the same cases as the regex path.
@@ -1868,6 +1884,8 @@ class Atom:
                     f"Use dep defaults are not allowed in EAPI {eapi}: '{self}'",
                     category="EAPI.incompatible",
                 )
+            if is_valid_flag is not None and self._use.conditional:
+                self._validate_conditional_flags(is_valid_flag)
 
         if (
             self._blocker_obj
@@ -1918,17 +1936,13 @@ class Atom:
             if allow_build_id is None:
                 allow_build_id = True
 
-        # Fast path: parse the atom in C for the common case. scan_atom is
-        # byte-exact with the regex path or raises ValueError (repo specs,
-        # build-ids, wildcards, anything invalid), in which case we fall
-        # through to the pure-Python regex path below. Wildcards, an injected
-        # _use, flag validation, and virtual-expansion originals are handled
-        # only by the regex path.
+        # Try the C fast path. scan_atom raises ValueError for anything it
+        # can't handle (wildcards, repo specs, build-ids); we fall through to
+        # the regex path. allow_wildcard is safe: __init__ tries strict regex
+        # first, so actual wildcard atoms fail scan_atom and fall back naturally.
         if (
             _c_dep_parser is not None
-            and not allow_wildcard
             and _use is None
-            and is_valid_flag is None
             and orig_atom is None
             and (eapi is None or eapi_attrs.slot_operator)
         ):
@@ -1937,7 +1951,9 @@ class Atom:
             except ValueError:
                 catom = None
             if catom is not None:
-                self._c_fast_init(catom, eapi, eapi_attrs, unevaluated_atom)
+                self._c_fast_init(
+                    catom, eapi, eapi_attrs, is_valid_flag, unevaluated_atom
+                )
                 return
 
         if s[:1] == "!":

diff --git a/lib/portage/tests/dep/test_c_parser.py b/lib/portage/tests/dep/test_c_parser.py
index 62b6aaec6..e939d4ded 100644
--- a/lib/portage/tests/dep/test_c_parser.py
+++ b/lib/portage/tests/dep/test_c_parser.py
@@ -1001,6 +1001,27 @@ class TestScanAtom(_AtomParityMixin, TestCase):
         self._assert_same("cat/pkg[a]", eapi="1")  # use deps invalid in EAPI 1
         self._assert_same("cat/pkg[a(+)]", eapi="4")  # defaults invalid in EAPI 4
 
+    def test_is_valid_flag(self):
+        # The conditional-flag-in-IUSE check runs in the fast path too.
+        def accept_all(flag):
+            return True
+
+        def reject_x(flag):
+            return not flag.startswith("x")
+
+        self._assert_same("cat/pkg[a?,!b?]", eapi="8", is_valid_flag=accept_all)
+        self._assert_same("cat/pkg[x?]", eapi="8", is_valid_flag=reject_x)  # invalid
+        self._assert_same("cat/pkg[x=]", eapi="8", is_valid_flag=reject_x)  # invalid
+        # Non-conditional flags are not validated by is_valid_flag.
+        self._assert_same("cat/pkg[x,-y]", eapi="8", is_valid_flag=reject_x)
+
+    def test_wildcard_falls_back(self):
+        # Extended/wildcard atoms fail scan_atom and use the regex path;
+        # results (extended_syntax etc.) must still match.
+        for s in ("*/*", "cat/*", "*/pkg", "dev-*/foo", "=cat/pkg-*1*"):
+            with self.subTest(s=s):
+                self._assert_same(s, allow_wildcard=True)
+
 
 class TestScanAtomNameGrammar(_AtomParityMixin, TestCase):
     """Category, package-name, version and revision edge cases, adapted from
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.