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

"Matt Turner" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1785963928.9a341b0d72f9f1d8a4509feb03f9266a66dc2979.mattst88@gentoo>
commit:     9a341b0d72f9f1d8a4509feb03f9266a66dc2979
Author:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug  2 20:17:12 2026 +0000
Commit:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug  5 21:05:28 2026 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=9a341b0d

dep: wire C parser into use_reduce fast path

When dep_parser is available, use_reduce() calls dep_parser.parse()
instead of the Python recursive-descent path for the common case (no
per-flag callbacks, EAPI 5+).  Falls back to the pure-Python path on
import failure or unsupported options.  Parse errors raise
InvalidDependString.

The fast path constructs portage.dep.Atom directly, so the guard
requires token_class to be exactly Atom rather than merely non-None; a
caller asking for some other token class gets the Python path, which
honors it.

The C scanner implements the modern (EAPI 5+) atom grammar
unconditionally -- slot operators, sub-slots, use-dep defaults -- so the
guard must check the EAPI, or under an older EAPI the fast path would
accept atoms that the pure-Python path correctly rejects (e.g.
"dev-libs/foo:=" or "dev-libs/foo[bar]" under EAPI 0/1/4).  Only take
the C path when that grammar is valid for the EAPI: eapi is None (an
already-validated, permissive context) or
_get_eapi_attrs(eapi).slot_operator (EAPI 5+).  slot_operator is the
newest atom-syntax feature, so it implies use_deps, slot_deps and
sub_slots are enabled too.  Older-EAPI strings fall through to the
Python path, which validates them.

Benchmarked over the 80065 dep strings in the ::gentoo md5-cache
(token_class=Atom, matchall=True, eapi=8), best of three, -O2.  The
pure-Python path is memoized, so its lru_cache is cleared between runs:

  _parser.parse() alone     0.149 s  538806 str/s
  use_reduce, Python path   2.913 s   27488 str/s
  use_reduce, C path        1.014 s   78938 str/s   (2.9x)

The remaining gap to the parse-only baseline is Python-side Atom object
allocation in _c_atom_from_c: 2.76 s above baseline for the Python path
against 0.87 s for the C path, so 3.2x on the object building alone.

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

 lib/portage/dep/__init__.py | 97 +++++++++++++++++++++++++++++++++++++++++++++
 src/dep_parser.c            |  6 ++-
 2 files changed, 102 insertions(+), 1 deletion(-)

diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index 613ad0b71..9dc2fc8ce 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -52,9 +52,85 @@ from portage.versions import (
     ververify,
 )
 
+try:
+    # Not "from . import _parser": pylint reports import-self for that when the
+    # extension has not been built, as in the lint-only CI job.
+    import portage.dep._parser as _c_dep_parser
+except ImportError:
+    _c_dep_parser = None
+
 if TYPE_CHECKING:
     from _emerge.Package import Package
 
+
+def _c_atom_from_c(catom, eapi, eapi_attrs, uselist, matchall):
+    """Construct a portage.dep.Atom from a _parser.Atom, bypassing __init__ regex."""
+    a = Atom.__new__(Atom)
+    a._string = str(catom)
+    a._cp = catom.cp
+    a._repo = None
+    a._slot = catom.slot
+    a._sub_slot = catom.sub_slot
+    a._slot_operator = catom.slot_operator
+    ver = catom.version
+    op = catom.operator
+    if op == "=" and ver is not None and ver.endswith("*"):
+        op = "=*"
+        ver = ver[:-1]
+    a._operator = op
+    a._version = ver
+    a._cpv = catom.cpv[:-1] if op == "=*" else catom.cpv
+    a._eapi = eapi
+    a._extended_syntax = False
+    a._build_id = None
+    a._unevaluated_atom = a
+    a._orig_atom = None
+    blocker_str = catom.blocker
+    a._blocker_obj = (
+        Atom._blocker(forbid_overlap=blocker_str == "!!")
+        if blocker_str is not None
+        else None
+    )
+    use_tokens = catom.use
+    if use_tokens is not None:
+        en, dis, miss_en, miss_dis, cond, req = _c_dep_parser.classify_use_deps(
+            use_tokens
+        )
+        a._use = _use_dep(
+            use_tokens,
+            eapi_attrs,
+            enabled_flags=en,
+            disabled_flags=dis,
+            missing_enabled=miss_en,
+            missing_disabled=miss_dis,
+            conditional=cond,
+            required=req,
+        )
+        if not matchall and a._use.conditional:
+            a = a.evaluate_conditionals(uselist)
+    else:
+        a._use = None
+    return a
+
+
+def _c_convert_result(items, eapi, eapi_attrs, uselist, matchall):
+    result = []
+    for item in items:
+        if isinstance(item, str):
+            result.append(item)
+        elif isinstance(item, list):
+            result.append(_c_convert_result(item, eapi, eapi_attrs, uselist, matchall))
+        else:
+            result.append(_c_atom_from_c(item, eapi, eapi_attrs, uselist, matchall))
+    return result
+
+
+def _c_fast_use_reduce(depstr, uselist, matchall, eapi):
+    raw = _c_dep_parser.parse(depstr, uselist=uselist, matchall=matchall)
+    eapi_attrs = _get_eapi_attrs(eapi)
+    return _c_convert_result(raw, eapi, eapi_attrs, uselist, matchall)
+
+
 # \w is [a-zA-Z0-9_]
 
 # PMS 3.1.3: A slot name may contain any of the characters [A-Za-z0-9+_.-].
@@ -950,6 +1026,27 @@ def use_reduce(
     if subset is not None:
         subset = frozenset(subset)
 
+    if (
+        _c_dep_parser is not None
+        # The fast path builds portage.dep.Atom directly, so it cannot serve
+        # a caller that asked for some other token class.
+        and token_class is Atom
+        and not is_src_uri
+        and not opconvert
+        and not flat
+        and is_valid_flag is None
+        and subset is None
+        and not matchnone
+        and not masklist
+        and not excludeall
+        # C grammar is EAPI 5+ only; eapi=None is permissive.
+        and (eapi is None or _get_eapi_attrs(eapi).slot_operator)
+    ):
+        try:
+            return _c_fast_use_reduce(depstr, uselist, matchall, eapi)
+        except ValueError as e:
+            raise InvalidDependString(str(e)) from e
+
     result = _use_reduce_cached(
         depstr,
         uselist,

diff --git a/src/dep_parser.c b/src/dep_parser.c
index 94dd3fdf2..8940f83b3 100644
--- a/src/dep_parser.c
+++ b/src/dep_parser.c
@@ -627,7 +627,11 @@ py_parse(UNUSED PyObject *self, PyObject *args, PyObject *kwargs)
 
     AUTO_PY useset = NULL;
     if (py_uselist != Py_None) {
-        useset = PyFrozenSet_New(py_uselist);
+        /* use_reduce() has already frozen its uselist, so the common case is
+         * a frozenset that can be reused instead of copied. */
+        useset = PyFrozenSet_CheckExact(py_uselist)
+            ? Py_NewRef(py_uselist)
+            : PyFrozenSet_New(py_uselist);
         if (!useset) {
             return NULL;
         }
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.