Improved error messages from clingo (#26719)

This PR adds error message sentinels to the clingo solve, attached to each of the rules that could fail a solve. The unsat core is then restricted to these messages, which makes the minimization problem tractable. Errors that can only be generated by a bug in the logic program or generating code are prefaced with "Internal error" to make clear to users that something has gone wrong on the Spack side of things.

* minimize unsat cores manually

* only errors messages are choices/assumptions for performance

* pre-check for unreachable nodes

* update tests for new error message

* make clingo concretization errors show up in cdash reports fully

* clingo: make import of clingo.ast parsing routines robust to clingo version

Older `clingo` has `parse_string`; newer `clingo` has `parse_files`.  Make the
code work wtih both.

* make AST access functions backward-compatible with clingo 5.4.0

Clingo AST API has changed since 5.4.0; make some functions to help us
handle both versions of the AST.

Co-authored-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
Greg Becker 2021-11-02 10:55:50 -07:00 committed by GitHub
parent 3187689862
commit b3711c0d9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 72 deletions

View File

@ -109,10 +109,7 @@ def solve(parser, args):
return return
# die if no solution was found # die if no solution was found
# TODO: we need to be able to provide better error messages than this result.raise_if_unsat()
if not result.satisfiable:
result.print_cores()
tty.die("Unsatisfiable spec.")
# dump the solutions as concretized specs # dump the solutions as concretized specs
if 'solutions' in dump: if 'solutions' in dump:

View File

@ -728,11 +728,7 @@ def concretize_specs_together(*abstract_specs, **kwargs):
def _concretize_specs_together_new(*abstract_specs, **kwargs): def _concretize_specs_together_new(*abstract_specs, **kwargs):
import spack.solver.asp import spack.solver.asp
result = spack.solver.asp.solve(abstract_specs) result = spack.solver.asp.solve(abstract_specs)
result.raise_if_unsat()
if not result.satisfiable:
result.print_cores()
tty.die("Unsatisfiable spec.")
return [s.copy() for s in result.specs] return [s.copy() for s in result.specs]

View File

@ -117,11 +117,24 @@ class SpecError(SpackError):
class UnsatisfiableSpecError(SpecError): class UnsatisfiableSpecError(SpecError):
"""Raised when a spec conflicts with package constraints. """
Provide the requirement that was violated when raising.""" Raised when a spec conflicts with package constraints.
def __init__(self, provided, required, constraint_type):
super(UnsatisfiableSpecError, self).__init__( For original concretizer, provide the requirement that was violated when
"%s does not satisfy %s" % (provided, required)) raising.
"""
def __init__(self, provided, required=None, constraint_type=None, conflicts=None):
# required is only set by the original concretizer.
# clingo concretizer handles error messages differently.
if required is not None:
assert not conflicts # can't mix formats
super(UnsatisfiableSpecError, self).__init__(
"%s does not satisfy %s" % (provided, required))
else:
indented = [' %s\n' % conflict for conflict in conflicts]
conflict_msg = ''.join(indented)
msg = '%s is unsatisfiable, conflicts are:\n%s' % (provided, conflict_msg)
super(UnsatisfiableSpecError, self).__init__(msg)
self.provided = provided self.provided = provided
self.required = required self.required = required
self.constraint_type = constraint_type self.constraint_type = constraint_type

View File

@ -52,6 +52,25 @@
else: else:
from collections import Sequence from collections import Sequence
# these are from clingo.ast and bootstrapped later
ASTType = None
parse_files = None
# backward compatibility functions for clingo ASTs
def ast_getter(*names):
def getter(node):
for name in names:
result = getattr(node, name, None)
if result:
return result
raise KeyError("node has no such keys: %s" % names)
return getter
ast_type = ast_getter("ast_type", "type")
ast_sym = ast_getter("symbol", "term")
#: Enumeration like object to mark version provenance #: Enumeration like object to mark version provenance
version_provenance = collections.namedtuple( # type: ignore version_provenance = collections.namedtuple( # type: ignore
@ -205,6 +224,9 @@ def __init__(self, specs, asp=None):
self.warnings = None self.warnings = None
self.nmodels = 0 self.nmodels = 0
# Saved control object for reruns when necessary
self.control = None
# specs ordered by optimization level # specs ordered by optimization level
self.answers = [] self.answers = []
self.cores = [] self.cores = []
@ -218,11 +240,85 @@ def __init__(self, specs, asp=None):
# Concrete specs # Concrete specs
self._concrete_specs = None self._concrete_specs = None
def print_cores(self): def format_core(self, core):
for core in self.cores: """
tty.msg( Format an unsatisfiable core for human readability
"The following constraints are unsatisfiable:",
*sorted(str(symbol) for symbol in core)) Returns a list of strings, where each string is the human readable
representation of a single fact in the core, including a newline.
Modeled after traceback.format_stack.
"""
assert self.control
symbols = dict(
(a.literal, a.symbol)
for a in self.control.symbolic_atoms
)
core_symbols = []
for atom in core:
sym = symbols[atom]
if sym.name in ("rule", "error"):
# these are special symbols we use to get messages in the core
sym = sym.arguments[0].string
core_symbols.append(sym)
return sorted(str(symbol) for symbol in core_symbols)
def minimize_core(self, core):
"""
Return a subset-minimal subset of the core.
Clingo cores may be thousands of lines when two facts are sufficient to
ensure unsatisfiability. This algorithm reduces the core to only those
essential facts.
"""
assert self.control
min_core = core[:]
for fact in core:
# Try solving without this fact
min_core.remove(fact)
ret = self.control.solve(assumptions=min_core)
if not ret.unsatisfiable:
min_core.append(fact)
return min_core
def minimal_cores(self):
"""
Return a list of subset-minimal unsatisfiable cores.
"""
return [self.minimize_core(core) for core in self.cores]
def format_minimal_cores(self):
"""List of facts for each core
Separate cores are separated by an empty line
"""
string_list = []
for core in self.minimal_cores():
if string_list:
string_list.append('\n')
string_list.extend(self.format_core(core))
return string_list
def raise_if_unsat(self):
"""
Raise an appropriate error if the result is unsatisfiable.
The error is a UnsatisfiableSpecError, and includes the minimized cores
resulting from the solve, formatted to be human readable.
"""
if self.satisfiable:
return
constraints = self.abstract_specs
if len(constraints) == 1:
constraints = constraints[0]
conflicts = self.format_minimal_cores()
raise spack.error.UnsatisfiableSpecError(constraints, conflicts=conflicts)
@property @property
def specs(self): def specs(self):
@ -276,6 +372,22 @@ def _normalize_packages_yaml(packages_yaml):
return normalized_yaml return normalized_yaml
def bootstrap_clingo():
global clingo, ASTType, parse_files
if not clingo:
with spack.bootstrap.ensure_bootstrap_configuration():
spack.bootstrap.ensure_clingo_importable_or_raise()
import clingo
from clingo.ast import ASTType
try:
from clingo.ast import parse_files
except ImportError:
# older versions of clingo have this one namespace up
from clingo import parse_files
class PyclingoDriver(object): class PyclingoDriver(object):
def __init__(self, cores=True, asp=None): def __init__(self, cores=True, asp=None):
"""Driver for the Python clingo interface. """Driver for the Python clingo interface.
@ -286,11 +398,8 @@ def __init__(self, cores=True, asp=None):
asp (file-like): optional stream to write a text-based ASP program asp (file-like): optional stream to write a text-based ASP program
for debugging or verification. for debugging or verification.
""" """
global clingo bootstrap_clingo()
if not clingo:
with spack.bootstrap.ensure_bootstrap_configuration():
spack.bootstrap.ensure_clingo_importable_or_raise()
import clingo
self.out = asp or llnl.util.lang.Devnull() self.out = asp or llnl.util.lang.Devnull()
self.cores = cores self.cores = cores
@ -311,15 +420,22 @@ def h2(self, name):
def newline(self): def newline(self):
self.out.write('\n') self.out.write('\n')
def fact(self, head): def fact(self, head, assumption=False):
"""ASP fact (a rule without a body).""" """ASP fact (a rule without a body).
Arguments:
head (AspFunction): ASP function to generate as fact
assumption (bool): If True and using cores, use this fact as a
choice point in ASP and include it in unsatisfiable cores
"""
symbol = head.symbol() if hasattr(head, 'symbol') else head symbol = head.symbol() if hasattr(head, 'symbol') else head
self.out.write("%s.\n" % str(symbol)) self.out.write("%s.\n" % str(symbol))
atom = self.backend.add_atom(symbol) atom = self.backend.add_atom(symbol)
self.backend.add_rule([atom], [], choice=self.cores) choice = self.cores and assumption
if self.cores: self.backend.add_rule([atom], [], choice=choice)
if choice:
self.assumptions.append(atom) self.assumptions.append(atom)
def solve( def solve(
@ -347,6 +463,22 @@ def solve(
# read in the main ASP program and display logic -- these are # read in the main ASP program and display logic -- these are
# handwritten, not generated, so we load them as resources # handwritten, not generated, so we load them as resources
parent_dir = os.path.dirname(__file__) parent_dir = os.path.dirname(__file__)
# extract error messages from concretize.lp by inspecting its AST
with self.backend:
def visit(node):
if ast_type(node) == ASTType.Rule:
for term in node.body:
if ast_type(term) == ASTType.Literal:
if ast_type(term.atom) == ASTType.SymbolicAtom:
if ast_sym(term.atom).name == "error":
arg = ast_sym(ast_sym(term.atom).arguments[0])
self.fact(fn.error(arg.string), assumption=True)
path = os.path.join(parent_dir, 'concretize.lp')
parse_files([path], visit)
# Load the file itself
self.control.load(os.path.join(parent_dir, 'concretize.lp')) self.control.load(os.path.join(parent_dir, 'concretize.lp'))
self.control.load(os.path.join(parent_dir, "display.lp")) self.control.load(os.path.join(parent_dir, "display.lp"))
timer.phase("load") timer.phase("load")
@ -367,6 +499,7 @@ def on_model(model):
solve_kwargs = {"assumptions": self.assumptions, solve_kwargs = {"assumptions": self.assumptions,
"on_model": on_model, "on_model": on_model,
"on_core": cores.append} "on_core": cores.append}
if clingo_cffi: if clingo_cffi:
solve_kwargs["on_unsat"] = cores.append solve_kwargs["on_unsat"] = cores.append
solve_result = self.control.solve(**solve_kwargs) solve_result = self.control.solve(**solve_kwargs)
@ -409,18 +542,8 @@ def stringify(x):
result.nmodels = len(models) result.nmodels = len(models)
elif cores: elif cores:
symbols = dict( result.control = self.control
(a.literal, a.symbol) result.cores.extend(cores)
for a in self.control.symbolic_atoms
)
for core in cores:
core_symbols = []
for atom in core:
sym = symbols[atom]
if sym.name == "rule":
sym = sym.arguments[0].string
core_symbols.append(sym)
result.cores.append(core_symbols)
if timers: if timers:
timer.write_tty() timer.write_tty()
@ -1372,6 +1495,14 @@ def setup(self, driver, specs, tests=False):
virtuals=self.possible_virtuals, virtuals=self.possible_virtuals,
deptype=spack.dependency.all_deptypes deptype=spack.dependency.all_deptypes
) )
# Fail if we already know an unreachable node is requested
for spec in specs:
missing_deps = [d for d in spec.traverse()
if d.name not in possible and not d.virtual]
if missing_deps:
raise spack.spec.InvalidDependencyError(spec.name, missing_deps)
pkgs = set(possible) pkgs = set(possible)
# driver is used by all the functions below to add facts and # driver is used by all the functions below to add facts and

View File

@ -18,7 +18,8 @@ version_declared(Package, Version, Weight) :- version_declared(Package, Version,
% We can't emit the same version **with the same weight** from two different sources % We can't emit the same version **with the same weight** from two different sources
:- version_declared(Package, Version, Weight, Origin1), :- version_declared(Package, Version, Weight, Origin1),
version_declared(Package, Version, Weight, Origin2), version_declared(Package, Version, Weight, Origin2),
Origin1 != Origin2. Origin1 != Origin2,
error("Internal error: two versions with identical weights").
% versions are declared w/priority -- declared with priority implies declared % versions are declared w/priority -- declared with priority implies declared
version_declared(Package, Version) :- version_declared(Package, Version, _). version_declared(Package, Version) :- version_declared(Package, Version, _).
@ -26,7 +27,7 @@ version_declared(Package, Version) :- version_declared(Package, Version, _).
% If something is a package, it has only one version and that must be a % If something is a package, it has only one version and that must be a
% declared version. % declared version.
1 { version(Package, Version) : version_declared(Package, Version) } 1 1 { version(Package, Version) : version_declared(Package, Version) } 1
:- node(Package). :- node(Package), error("Each node must have exactly one version").
% A virtual package may have or not a version, but never has more than one % A virtual package may have or not a version, but never has more than one
:- virtual_node(Package), 2 { version(Package, _) }. :- virtual_node(Package), 2 { version(Package, _) }.
@ -38,12 +39,13 @@ possible_version_weight(Package, Weight)
:- version(Package, Version), :- version(Package, Version),
version_declared(Package, Version, Weight). version_declared(Package, Version, Weight).
1 { version_weight(Package, Weight) : possible_version_weight(Package, Weight) } 1 :- node(Package). 1 { version_weight(Package, Weight) : possible_version_weight(Package, Weight) } 1 :- node(Package), error("Internal error: Package version must have a unique weight").
% version_satisfies implies that exactly one of the satisfying versions % version_satisfies implies that exactly one of the satisfying versions
% is the package's version, and vice versa. % is the package's version, and vice versa.
1 { version(Package, Version) : version_satisfies(Package, Constraint, Version) } 1 1 { version(Package, Version) : version_satisfies(Package, Constraint, Version) } 1
:- version_satisfies(Package, Constraint). :- version_satisfies(Package, Constraint),
error("no version satisfies the given constraints").
version_satisfies(Package, Constraint) version_satisfies(Package, Constraint)
:- version(Package, Version), version_satisfies(Package, Constraint, Version). :- version(Package, Version), version_satisfies(Package, Constraint, Version).
@ -122,14 +124,17 @@ node(Dependency) :- node(Package), depends_on(Package, Dependency).
% dependencies) and get a two-node unconnected graph % dependencies) and get a two-node unconnected graph
needed(Package) :- root(Package). needed(Package) :- root(Package).
needed(Dependency) :- needed(Package), depends_on(Package, Dependency). needed(Dependency) :- needed(Package), depends_on(Package, Dependency).
:- node(Package), not needed(Package). :- node(Package), not needed(Package),
error("All dependencies must be reachable from root").
% Avoid cycles in the DAG % Avoid cycles in the DAG
% some combinations of conditional dependencies can result in cycles; % some combinations of conditional dependencies can result in cycles;
% this ensures that we solve around them % this ensures that we solve around them
path(Parent, Child) :- depends_on(Parent, Child). path(Parent, Child) :- depends_on(Parent, Child).
path(Parent, Descendant) :- path(Parent, A), depends_on(A, Descendant). path(Parent, Descendant) :- path(Parent, A), depends_on(A, Descendant).
:- path(A, B), path(B, A). :- path(A, B), path(B, A), error("Cyclic dependencies are not allowed").
#defined error/1.
#defined dependency_type/2. #defined dependency_type/2.
#defined dependency_condition/3. #defined dependency_condition/3.
@ -141,7 +146,8 @@ path(Parent, Descendant) :- path(Parent, A), depends_on(A, Descendant).
not external(Package), not external(Package),
conflict(Package, TriggerID, ConstraintID), conflict(Package, TriggerID, ConstraintID),
condition_holds(TriggerID), condition_holds(TriggerID),
condition_holds(ConstraintID). condition_holds(ConstraintID),
error("A conflict was triggered").
#defined conflict/3. #defined conflict/3.
@ -164,7 +170,7 @@ virtual_node(Virtual)
% If there's a virtual node, we must select one and only one provider. % If there's a virtual node, we must select one and only one provider.
% The provider must be selected among the possible providers. % The provider must be selected among the possible providers.
1 { provider(Package, Virtual) : possible_provider(Package, Virtual) } 1 1 { provider(Package, Virtual) : possible_provider(Package, Virtual) } 1
:- virtual_node(Virtual). :- virtual_node(Virtual), error("Virtual packages must be satisfied by a unique provider").
% virtual roots imply virtual nodes, and that one provider is a root % virtual roots imply virtual nodes, and that one provider is a root
virtual_node(Virtual) :- virtual_root(Virtual). virtual_node(Virtual) :- virtual_root(Virtual).
@ -188,7 +194,8 @@ virtual_condition_holds(Provider, Virtual) :-
% A package cannot be the actual provider for a virtual if it does not % A package cannot be the actual provider for a virtual if it does not
% fulfill the conditions to provide that virtual % fulfill the conditions to provide that virtual
:- provider(Package, Virtual), not virtual_condition_holds(Package, Virtual). :- provider(Package, Virtual), not virtual_condition_holds(Package, Virtual),
error("Internal error: virtual when provides not respected").
#defined possible_provider/2. #defined possible_provider/2.
@ -201,7 +208,7 @@ virtual_condition_holds(Provider, Virtual) :-
% we select the weight, among the possible ones, that minimizes the overall objective function. % we select the weight, among the possible ones, that minimizes the overall objective function.
1 { provider_weight(Dependency, Virtual, Weight, Reason) : 1 { provider_weight(Dependency, Virtual, Weight, Reason) :
possible_provider_weight(Dependency, Virtual, Weight, Reason) } 1 possible_provider_weight(Dependency, Virtual, Weight, Reason) } 1
:- provider(Dependency, Virtual). :- provider(Dependency, Virtual), error("Internal error: package provider weights must be unique").
% Get rid or the reason for enabling the possible weight (useful for debugging) % Get rid or the reason for enabling the possible weight (useful for debugging)
provider_weight(Dependency, Virtual, Weight) :- provider_weight(Dependency, Virtual, Weight, _). provider_weight(Dependency, Virtual, Weight) :- provider_weight(Dependency, Virtual, Weight, _).
@ -299,7 +306,7 @@ attr("node_compiler_version_satisfies", Package, Compiler, Version)
% if a package is external its version must be one of the external versions % if a package is external its version must be one of the external versions
1 { external_version(Package, Version, Weight): 1 { external_version(Package, Version, Weight):
version_declared(Package, Version, Weight, "external") } 1 version_declared(Package, Version, Weight, "external") } 1
:- external(Package). :- external(Package), error("External package version does not satisfy external spec").
version_weight(Package, Weight) :- external_version(Package, Version, Weight). version_weight(Package, Weight) :- external_version(Package, Version, Weight).
version(Package, Version) :- external_version(Package, Version, Weight). version(Package, Version) :- external_version(Package, Version, Weight).
@ -318,7 +325,8 @@ external(Package) :- external_spec_selected(Package, _).
:- version(Package, Version), :- version(Package, Version),
version_weight(Package, Weight), version_weight(Package, Weight),
version_declared(Package, Version, Weight, "external"), version_declared(Package, Version, Weight, "external"),
not external(Package). not external(Package),
error("Internal error: external weight used for internal spec").
% determine if an external spec has been selected % determine if an external spec has been selected
external_spec_selected(Package, LocalIndex) :- external_spec_selected(Package, LocalIndex) :-
@ -330,7 +338,8 @@ external_conditions_hold(Package, LocalIndex) :-
% it cannot happen that a spec is external, but none of the external specs % it cannot happen that a spec is external, but none of the external specs
% conditions hold. % conditions hold.
:- external(Package), not external_conditions_hold(Package, _). :- external(Package), not external_conditions_hold(Package, _),
error("External package does not satisfy external spec").
#defined possible_external/3. #defined possible_external/3.
#defined external_spec_index/3. #defined external_spec_index/3.
@ -348,7 +357,8 @@ external_conditions_hold(Package, LocalIndex) :-
} 1 } 1
:- node(Package), :- node(Package),
variant(Package, Variant), variant(Package, Variant),
variant_single_value(Package, Variant). variant_single_value(Package, Variant),
error("Single valued variants must have a single value").
% at least one variant value for multi-valued variants. % at least one variant value for multi-valued variants.
1 { 1 {
@ -357,13 +367,15 @@ external_conditions_hold(Package, LocalIndex) :-
} }
:- node(Package), :- node(Package),
variant(Package, Variant), variant(Package, Variant),
not variant_single_value(Package, Variant). not variant_single_value(Package, Variant),
error("Internal error: All variants must have a value").
% if a variant is set to anything, it is considered 'set'. % if a variant is set to anything, it is considered 'set'.
variant_set(Package, Variant) :- variant_set(Package, Variant, _). variant_set(Package, Variant) :- variant_set(Package, Variant, _).
% A variant cannot have a value that is not also a possible value % A variant cannot have a value that is not also a possible value
:- variant_value(Package, Variant, Value), not variant_possible_value(Package, Variant, Value). :- variant_value(Package, Variant, Value), not variant_possible_value(Package, Variant, Value),
error("Variant set to invalid value").
% Some multi valued variants accept multiple values from disjoint sets. % Some multi valued variants accept multiple values from disjoint sets.
% Ensure that we respect that constraint and we don't pick values from more % Ensure that we respect that constraint and we don't pick values from more
@ -372,7 +384,8 @@ variant_set(Package, Variant) :- variant_set(Package, Variant, _).
variant_value(Package, Variant, Value2), variant_value(Package, Variant, Value2),
variant_value_from_disjoint_sets(Package, Variant, Value1, Set1), variant_value_from_disjoint_sets(Package, Variant, Value1, Set1),
variant_value_from_disjoint_sets(Package, Variant, Value2, Set2), variant_value_from_disjoint_sets(Package, Variant, Value2, Set2),
Set1 != Set2. Set1 != Set2,
error("Variant values selected from multiple disjoint sets").
% variant_set is an explicitly set variant value. If it's not 'set', % variant_set is an explicitly set variant value. If it's not 'set',
% we revert to the default value. If it is set, we force the set value % we revert to the default value. If it is set, we force the set value
@ -437,7 +450,8 @@ variant_default_value(Package, Variant, Value) :- variant_default_value_from_cli
% Treat 'none' in a special way - it cannot be combined with other % Treat 'none' in a special way - it cannot be combined with other
% values even if the variant is multi-valued % values even if the variant is multi-valued
:- 2 {variant_value(Package, Variant, Value): variant_possible_value(Package, Variant, Value)}, :- 2 {variant_value(Package, Variant, Value): variant_possible_value(Package, Variant, Value)},
variant_value(Package, Variant, "none"). variant_value(Package, Variant, "none"),
error("Variant value 'none' cannot be combined with any other value").
% patches and dev_path are special variants -- they don't have to be % patches and dev_path are special variants -- they don't have to be
% declared in the package, so we just allow them to spring into existence % declared in the package, so we just allow them to spring into existence
@ -467,7 +481,7 @@ variant_single_value(Package, "dev_path")
%----------------------------------------------------------------------------- %-----------------------------------------------------------------------------
% one platform per node % one platform per node
:- M = #count { Platform : node_platform(Package, Platform) }, M !=1, node(Package). :- M = #count { Platform : node_platform(Package, Platform) }, M !=1, node(Package), error("A node must have exactly one platform").
% if no platform is set, fall back to the default % if no platform is set, fall back to the default
node_platform(Package, Platform) node_platform(Package, Platform)
@ -488,7 +502,7 @@ node_platform_set(Package) :- node_platform_set(Package, _).
% OS semantics % OS semantics
%----------------------------------------------------------------------------- %-----------------------------------------------------------------------------
% one os per node % one os per node
1 { node_os(Package, OS) : os(OS) } 1 :- node(Package). 1 { node_os(Package, OS) : os(OS) } 1 :- node(Package), error("Each node must have exactly one OS").
% node_os_set implies that the node must have that os % node_os_set implies that the node must have that os
node_os(Package, OS) :- node(Package), node_os_set(Package, OS). node_os(Package, OS) :- node(Package), node_os_set(Package, OS).
@ -515,11 +529,11 @@ node_os(Package, OS)
% Target semantics % Target semantics
%----------------------------------------------------------------------------- %-----------------------------------------------------------------------------
% one target per node -- optimization will pick the "best" one % one target per node -- optimization will pick the "best" one
1 { node_target(Package, Target) : target(Target) } 1 :- node(Package). 1 { node_target(Package, Target) : target(Target) } 1 :- node(Package), error("Each node must have exactly one target").
% node_target_satisfies semantics % node_target_satisfies semantics
1 { node_target(Package, Target) : node_target_satisfies(Package, Constraint, Target) } 1 1 { node_target(Package, Target) : node_target_satisfies(Package, Constraint, Target) } 1
:- node_target_satisfies(Package, Constraint). :- node_target_satisfies(Package, Constraint), error("Each node must have exactly one target").
node_target_satisfies(Package, Constraint) node_target_satisfies(Package, Constraint)
:- node_target(Package, Target), node_target_satisfies(Package, Constraint, Target). :- node_target(Package, Target), node_target_satisfies(Package, Constraint, Target).
#defined node_target_satisfies/3. #defined node_target_satisfies/3.
@ -546,7 +560,8 @@ target_weight(Target, Package, Weight)
:- node_target(Package, Target), :- node_target(Package, Target),
not compiler_supports_target(Compiler, Version, Target), not compiler_supports_target(Compiler, Version, Target),
node_compiler(Package, Compiler), node_compiler(Package, Compiler),
node_compiler_version(Package, Compiler, Version). node_compiler_version(Package, Compiler, Version),
error("No satisfying compiler available is compatible with a satisfying target").
% if a target is set explicitly, respect it % if a target is set explicitly, respect it
node_target(Package, Target) node_target(Package, Target)
@ -583,7 +598,7 @@ compiler(Compiler) :- compiler_version(Compiler, _).
% There must be only one compiler set per node. The compiler % There must be only one compiler set per node. The compiler
% is chosen among available versions. % is chosen among available versions.
1 { node_compiler_version(Package, Compiler, Version) 1 { node_compiler_version(Package, Compiler, Version)
: compiler_version(Compiler, Version) } 1 :- node(Package). : compiler_version(Compiler, Version) } 1 :- node(Package), error("Each node must have exactly one compiler").
% Sometimes we just need to know the compiler and not the version % Sometimes we just need to know the compiler and not the version
node_compiler(Package, Compiler) :- node_compiler_version(Package, Compiler, _). node_compiler(Package, Compiler) :- node_compiler_version(Package, Compiler, _).
@ -591,14 +606,15 @@ node_compiler(Package, Compiler) :- node_compiler_version(Package, Compiler, _).
% We can't have a compiler be enforced and select the version from another compiler % We can't have a compiler be enforced and select the version from another compiler
:- node_compiler(Package, Compiler1), :- node_compiler(Package, Compiler1),
node_compiler_version(Package, Compiler2, _), node_compiler_version(Package, Compiler2, _),
Compiler1 != Compiler2. Compiler1 != Compiler2,
error("Internal error: mismatch between selected compiler and compiler version").
% define node_compiler_version_satisfies/3 from node_compiler_version_satisfies/4 % define node_compiler_version_satisfies/3 from node_compiler_version_satisfies/4
% version_satisfies implies that exactly one of the satisfying versions % version_satisfies implies that exactly one of the satisfying versions
% is the package's version, and vice versa. % is the package's version, and vice versa.
1 { node_compiler_version(Package, Compiler, Version) 1 { node_compiler_version(Package, Compiler, Version)
: node_compiler_version_satisfies(Package, Compiler, Constraint, Version) } 1 : node_compiler_version_satisfies(Package, Compiler, Constraint, Version) } 1
:- node_compiler_version_satisfies(Package, Compiler, Constraint). :- node_compiler_version_satisfies(Package, Compiler, Constraint), error("Internal error: node compiler version mismatch").
node_compiler_version_satisfies(Package, Compiler, Constraint) node_compiler_version_satisfies(Package, Compiler, Constraint)
:- node_compiler_version(Package, Compiler, Version), :- node_compiler_version(Package, Compiler, Version),
node_compiler_version_satisfies(Package, Compiler, Constraint, Version). node_compiler_version_satisfies(Package, Compiler, Constraint, Version).
@ -614,7 +630,8 @@ node_compiler_version(Package, Compiler, Version) :- node_compiler_version_set(P
% are excluded from this check % are excluded from this check
:- node_compiler_version(Package, Compiler, Version), node_os(Package, OS), :- node_compiler_version(Package, Compiler, Version), node_os(Package, OS),
not compiler_supports_os(Compiler, Version, OS), not compiler_supports_os(Compiler, Version, OS),
not allow_compiler(Compiler, Version). not allow_compiler(Compiler, Version),
error("No satisfying compiler available is compatible with a satisfying os").
% If a package and one of its dependencies don't have the % If a package and one of its dependencies don't have the
% same compiler there's a mismatch. % same compiler there's a mismatch.

View File

@ -2608,10 +2608,7 @@ def _new_concretize(self, tests=False):
return return
result = spack.solver.asp.solve([self], tests=tests) result = spack.solver.asp.solve([self], tests=tests)
if not result.satisfiable: result.raise_if_unsat()
result.print_cores()
raise spack.error.UnsatisfiableSpecError(
self, "unknown", "Unsatisfiable!")
# take the best answer # take the best answer
opt, i, answer = min(result.answers) opt, i, answer = min(result.answers)

View File

@ -515,7 +515,7 @@ def test_cdash_report_concretization_error(tmpdir, mock_fetch, install_mockery,
# new or the old concretizer # new or the old concretizer
expected_messages = ( expected_messages = (
'Conflicts in concretized spec', 'Conflicts in concretized spec',
'does not satisfy' 'A conflict was triggered',
) )
assert any(x in content for x in expected_messages) assert any(x in content for x in expected_messages)