summaryrefslogtreecommitdiff
path: root/kconfiglib.py
diff options
context:
space:
mode:
Diffstat (limited to 'kconfiglib.py')
-rw-r--r--kconfiglib.py184
1 files changed, 183 insertions, 1 deletions
diff --git a/kconfiglib.py b/kconfiglib.py
index 7f5c122..608a2bb 100644
--- a/kconfiglib.py
+++ b/kconfiglib.py
@@ -726,9 +726,17 @@ class Kconfig(object):
_check_choice_sanity(choice)
- # Build Symbol._dependents for all symbols
+ # Build Symbol._dependents for all symbols and choices
self._build_dep()
+ # Check for dependency loops
+ for sym in self.defined_syms:
+ _check_dep_loop_sym(sym, False)
+
+ # Add extra dependencies from choices to choice symbols that get
+ # awkward during dependency loop detection
+ self._add_choice_deps()
+
self._warn_for_no_prompt = True
@property
@@ -2485,6 +2493,17 @@ class Kconfig(object):
for _, cond in choice.defaults:
_make_depend_on(choice, cond)
+ def _add_choice_deps(self):
+ # Choices also depend on the choice symbols themselves, because the
+ # y-mode selection of the choice might change if a choice symbol's
+ # visibility changes.
+ #
+ # We add these dependencies separately after dependency loop detection.
+ # The invalidation algorithm can handle the resulting
+ # <choice symbol> <-> <choice> dependency loops, but they make loop
+ # detection awkward.
+
+ for choice in self.choices:
# The choice symbols themselves, because the y mode selection might
# change if a choice symbol's visibility changes
for sym in choice.syms:
@@ -2924,6 +2943,7 @@ class Symbol(object):
"_cached_str_val",
"_cached_tri_val",
"_cached_vis",
+ "_checked",
"_dependents",
"_old_val",
"_was_set",
@@ -3435,6 +3455,9 @@ class Symbol(object):
# See Kconfig._build_dep()
self._dependents = set()
+ # Used during dependency loop detection
+ self._checked = 0
+
def _assignable(self):
# Worker function for the 'assignable' attribute
@@ -3752,6 +3775,7 @@ class Choice(object):
"_cached_assignable",
"_cached_selection",
"_cached_vis",
+ "_checked",
"_dependents",
"_was_set",
"defaults",
@@ -3974,6 +3998,9 @@ class Choice(object):
# See Kconfig._build_dep()
self._dependents = set()
+ # Used during dependency loop detection
+ self._checked = 0
+
def _assignable(self):
# Worker function for the 'assignable' attribute
@@ -4827,6 +4854,161 @@ def _finalize_choice(node):
if sym.orig_type == UNKNOWN:
sym.orig_type = choice.orig_type
+def _check_dep_loop_sym(sym, ignore_choice):
+ # Detects dependency loops using depth-first search on the dependency graph
+ # (which is calculated earlier in Kconfig._build_dep()).
+ #
+ # Algorithm:
+ #
+ # 1. Symbols/choices start out with _checked = 0, meaning unvisited.
+ #
+ # 2. When a symbol/choice is first visited, _checked is set to 1, meaning
+ # "visited, potentially part of a dependency loop". The recursive
+ # search then continues from the symbol/choice.
+ #
+ # 3. If we run into a symbol/choice X with _checked already set to 1,
+ # there's a dependency loop. The loop is found on the call stack by
+ # recording symbols while returning ("on the way back") until X is seen
+ # again.
+ #
+ # 4. Once a symbol/choice and all its dependencies (or dependents in this
+ # case) have been checked recursively without detecting any loops, its
+ # _checked is set to 2, meaning "visited, not part of a dependency
+ # loop".
+ #
+ # This saves work if we run into the symbol/choice again in later calls
+ # to _check_dep_loop_sym(). We just return immediately.
+ #
+ # Choices complicate things, as every choice symbol depends on every other
+ # choice symbol in a sense. When a choice is "entered" via a choice symbol
+ # X, we visit all choice symbols from the choice except X, and prevent
+ # immediately revisiting the choice with a flag (ignore_choice).
+ #
+ # Maybe there's a better way to handle this (different flags or the
+ # like...)
+
+ if not sym._checked:
+ # sym._checked == 0, unvisited
+
+ sym._checked = 1
+
+ for dep in sym._dependents:
+ # Choices show up in Symbol._dependents when the choice has the
+ # symbol in a 'prompt' or 'default' condition (e.g.
+ # 'default ... if SYM').
+ #
+ # Since we aren't entering the choice via a choice symbol, all
+ # choice symbols need to be checked, hence the None.
+ loop = _check_dep_loop_choice(dep, None) \
+ if isinstance(dep, Choice) \
+ else _check_dep_loop_sym(dep, False)
+
+ if loop:
+ # Dependency loop found
+ return _found_dep_loop(loop, sym)
+
+ if sym.choice and not ignore_choice:
+ loop = _check_dep_loop_choice(sym.choice, sym)
+ if loop:
+ # Dependency loop found
+ return _found_dep_loop(loop, sym)
+
+ # The symbol is not part of a dependency loop
+ sym._checked = 2
+
+ # No dependency loop found
+ return None
+
+ if sym._checked == 2:
+ # The symbol was checked earlier and is already known to not be part of
+ # a dependency loop
+ return None
+
+ # sym._checked == 1, found a dependency loop. Return the symbol as the
+ # first element in it.
+ return (sym,)
+
+def _check_dep_loop_choice(choice, skip):
+ if not choice._checked:
+ # choice._checked == 0, unvisited
+
+ choice._checked = 1
+
+ # Check for loops involving choice symbols. If we came here via a
+ # choice symbol, skip that one, as we'd get a false positive
+ # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise.
+ for sym in choice.syms:
+ if sym is not skip:
+ # Prevent the choice from being immediately re-entered via the
+ # "is a choice symbol" path by passing True
+ loop = _check_dep_loop_sym(sym, True)
+ if loop:
+ # Dependency loop found
+ return _found_dep_loop(loop, choice)
+
+ # The choice is not part of a dependency loop
+ choice._checked = 2
+
+ # No dependency loop found
+ return None
+
+ if choice._checked == 2:
+ # The choice was checked earlier and is already known to not be part of
+ # a dependency loop
+ return None
+
+ # choice._checked == 1, found a dependency loop. Return the choice as the
+ # first element in it.
+ return (choice,)
+
+def _found_dep_loop(loop, cur):
+ # Called "on the way back" when we know we have a loop
+
+ # Is the symbol/choice 'cur' where the loop started?
+ if cur is not loop[0]:
+ # Nope, it's just a part of the loop
+ return loop + (cur,)
+
+ # Yep, we have the entire loop. Throw an exception that shows it.
+
+ msg = "\nDependency loop\n" \
+ "===============\n\n"
+
+ for item in loop:
+ if item is not loop[0]:
+ msg += "...depends on "
+ if isinstance(item, Symbol) and item.choice:
+ msg += "the choice symbol "
+
+ msg += "{}, with definition...\n\n{}\n" \
+ .format(_name_and_loc(item), item)
+
+ # Small wart: Since we reuse the already calculated
+ # Symbol/Choice._dependents sets for recursive dependency detection, we
+ # lose information on whether a dependency came from a 'select'/'imply'
+ # condition or e.g. a 'depends on'.
+ #
+ # This might cause selecting symbols to "disappear". For example,
+ # a symbol B having 'select A if C' gives a direct dependency from A to
+ # C, since it corresponds to a reverse dependency of B && C.
+ #
+ # Always print reverse dependencies for symbols that have them to make
+ # sure information isn't lost. I wonder if there's some neat way to
+ # improve this.
+
+ if isinstance(item, Symbol):
+ if item.rev_dep is not item.kconfig.n:
+ msg += "(select-related dependencies: {})\n\n" \
+ .format(expr_str(item.rev_dep))
+
+ if item.weak_rev_dep is not item.kconfig.n:
+ msg += "(imply-related dependencies: {})\n\n" \
+ .format(expr_str(item.rev_dep))
+
+ msg += "...depends again on {}".format(_name_and_loc(loop[0]))
+
+ raise KconfigSyntaxError(msg)
+
def _check_sym_sanity(sym):
# Checks various symbol properties that are handiest to check after
# parsing. Only generates errors and warnings.