concretizer: add conflict rules from packages

Conflict rules from packages are added as integrity
constraints in the ASP formulation. Most of the code
to generate them has been reused from PyclingoDriver.rules
This commit is contained in:
Massimiliano Culpo 2020-10-26 20:37:38 +01:00 committed by Todd Gamblin
parent 2595b58503
commit 1cdee03c4b
3 changed files with 72 additions and 28 deletions

View file

@ -401,6 +401,19 @@ def colorize(string):
return result return result
def _normalize_body(body):
"""Accept an AspAnd object or a single Symbol and return a list of
symbols.
"""
if isinstance(body, AspAnd):
args = [f.symbol() for f in body.args]
elif isinstance(body, clingo.Symbol):
args = [body]
else:
raise TypeError("Invalid typee for rule body: ", type(body))
return args
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.
@ -438,6 +451,18 @@ def one_of(self, *args):
def _and(self, *args): def _and(self, *args):
return AspAnd(*args) return AspAnd(*args)
def _register_rule_for_cores(self, rule_str):
# rule atoms need to be choices before we can assume them
if self.cores:
rule_sym = clingo.Function("rule", [rule_str])
rule_atom = self.backend.add_atom(rule_sym)
self.backend.add_rule([rule_atom], [], choice=True)
self.assumptions.append(rule_atom)
rule_atoms = [rule_atom]
else:
rule_atoms = []
return rule_atoms
def fact(self, head): def fact(self, head):
"""ASP fact (a rule without a body).""" """ASP fact (a rule without a body)."""
sym = head.symbol() sym = head.symbol()
@ -450,12 +475,7 @@ def fact(self, head):
def rule(self, head, body): def rule(self, head, body):
"""ASP rule (an implication).""" """ASP rule (an implication)."""
if isinstance(body, AspAnd): args = _normalize_body(body)
args = [f.symbol() for f in body.args]
elif isinstance(body, clingo.Symbol):
args = [body]
else:
raise TypeError("Invalid typee for rule body: ", type(body))
symbols = [head.symbol()] + args symbols = [head.symbol()] + args
atoms = {} atoms = {}
@ -466,15 +486,7 @@ def rule(self, head, body):
rule_str = "%s :- %s." % ( rule_str = "%s :- %s." % (
head.symbol(), ",".join(str(a) for a in args)) head.symbol(), ",".join(str(a) for a in args))
# rule atoms need to be choices before we can assume them rule_atoms = self._register_rule_for_cores(rule_str)
if self.cores:
rule_sym = clingo.Function("rule", [rule_str])
rule_atom = self.backend.add_atom(rule_sym)
self.backend.add_rule([rule_atom], [], choice=True)
self.assumptions.append(rule_atom)
rule_atoms = [rule_atom]
else:
rule_atoms = []
# print rule before adding # print rule before adding
self.out.write("%s\n" % rule_str) self.out.write("%s\n" % rule_str)
@ -483,6 +495,18 @@ def rule(self, head, body):
[atoms[s] for s in args] + rule_atoms [atoms[s] for s in args] + rule_atoms
) )
def integrity_constraint(self, body):
symbols, atoms = _normalize_body(body), {}
for s in symbols:
atoms[s] = self.backend.add_atom(s)
rule_str = ":- {0}.".format(",".join(str(a) for a in symbols))
rule_atoms = self._register_rule_for_cores(rule_str)
# print rule before adding
self.out.write("{0}\n".format(rule_str))
self.backend.add_rule([], [atoms[s] for s in symbols] + rule_atoms)
def one_of_iff(self, head, versions): def one_of_iff(self, head, versions):
self.out.write("%s :- %s.\n" % (head, AspOneOf(*versions))) self.out.write("%s :- %s.\n" % (head, AspOneOf(*versions)))
self.out.write("%s :- %s.\n" % (AspOneOf(*versions), head)) self.out.write("%s :- %s.\n" % (AspOneOf(*versions), head))
@ -661,6 +685,27 @@ def spec_versions(self, spec):
self.version_constraints.add((spec.name, spec.versions)) self.version_constraints.add((spec.name, spec.versions))
return [fn.version_satisfies(spec.name, spec.versions)] return [fn.version_satisfies(spec.name, spec.versions)]
def conflict_rules(self, pkg):
for trigger, constraints in pkg.conflicts.items():
for constraint, _ in constraints:
constraint_body = spack.spec.Spec(pkg.name)
constraint_body.constrain(constraint)
constraint_body.constrain(trigger)
clauses = []
for s in constraint_body.traverse():
clauses += self.spec_clauses(s, body=True)
# TODO: find a better way to generate clauses for integrity
# TODO: constraints, instead of generating them for the body
# TODO: of a rule and filter unwanted functions.
to_be_filtered = [
'node_compiler_hard', 'node_compiler_version_satisfies'
]
clauses = [x for x in clauses if x.name not in to_be_filtered]
self.gen.integrity_constraint(AspAnd(*clauses))
def available_compilers(self): def available_compilers(self):
"""Facts about available compilers.""" """Facts about available compilers."""
@ -750,6 +795,9 @@ def pkg_rules(self, pkg):
self.gen.newline() self.gen.newline()
# conflicts
self.conflict_rules(pkg)
# default compilers for this package # default compilers for this package
self.package_compiler_defaults(pkg) self.package_compiler_defaults(pkg)
@ -948,7 +996,7 @@ def _supported_targets(self, compiler, targets):
try: try:
target.optimization_flags(compiler.name, compiler.version) target.optimization_flags(compiler.name, compiler.version)
supported.append(target) supported.append(target)
except llnl.util.cpu.UnsupportedMicroarchitecture as e: except llnl.util.cpu.UnsupportedMicroarchitecture:
continue continue
return sorted(supported, reverse=True) return sorted(supported, reverse=True)

View file

@ -10,11 +10,11 @@
import spack.architecture import spack.architecture
import spack.concretize import spack.concretize
import spack.error
import spack.repo import spack.repo
from spack.concretize import find_spec, NoValidVersionError from spack.concretize import find_spec
from spack.error import SpecError, SpackError from spack.spec import Spec, CompilerSpec
from spack.spec import Spec, CompilerSpec, ConflictsInSpecError
from spack.version import ver from spack.version import ver
from spack.util.mock_package import MockPackageMultiRepo from spack.util.mock_package import MockPackageMultiRepo
import spack.compilers import spack.compilers
@ -495,12 +495,10 @@ def test_compiler_child(self):
assert s['dyninst'].satisfies('%gcc') assert s['dyninst'].satisfies('%gcc')
def test_conflicts_in_spec(self, conflict_spec): def test_conflicts_in_spec(self, conflict_spec):
# Check that an exception is raised an caught by the appropriate s = Spec(conflict_spec)
# exception types. with pytest.raises(spack.error.SpackError):
for exc_type in (ConflictsInSpecError, RuntimeError, SpecError): s.concretize()
s = Spec(conflict_spec) assert not s.concrete
with pytest.raises(exc_type):
s.concretize()
def test_no_conflixt_in_external_specs(self, conflict_spec): def test_no_conflixt_in_external_specs(self, conflict_spec):
# clear deps because external specs cannot depend on anything # clear deps because external specs cannot depend on anything
@ -608,7 +606,7 @@ def test_simultaneous_concretization_of_specs(self, abstract_specs):
@pytest.mark.parametrize('spec', ['noversion', 'noversion-bundle']) @pytest.mark.parametrize('spec', ['noversion', 'noversion-bundle'])
def test_noversion_pkg(self, spec): def test_noversion_pkg(self, spec):
"""Test concretization failures for no-version packages.""" """Test concretization failures for no-version packages."""
with pytest.raises(SpackError): with pytest.raises(spack.error.SpackError):
Spec(spec).concretized() Spec(spec).concretized()
@pytest.mark.parametrize('spec, best_achievable', [ @pytest.mark.parametrize('spec, best_achievable', [

View file

@ -1185,9 +1185,7 @@ def installation_dir_with_headers(tmpdir_factory):
@pytest.fixture( @pytest.fixture(
params=[ params=[
'conflict%clang',
'conflict%clang+foo', 'conflict%clang+foo',
'conflict-parent%clang',
'conflict-parent@0.9^conflict~foo' 'conflict-parent@0.9^conflict~foo'
] ]
) )