concretizer: handle versions with choice construct rather than conflicts

Use '1 { version(x); version(y); version(z) } 1.' instead of declaring
conflicts for non-matching versions.  This keeps the sense of version
clauses positive, which will allow them to be used more easily in
conditionals later.

Also refactor `spec_clauses()` method to return clauses that can be used
in conditions, etc. instead of just printing out facts.
This commit is contained in:
Todd Gamblin 2020-06-07 15:54:58 -07:00
parent 6e31430bec
commit 8a6207aa70

View File

@ -30,11 +30,6 @@
_max_line = 80 _max_line = 80
def _id(thing):
"""Quote string if needed for it to be a valid identifier."""
return '"%s"' % str(thing)
def issequence(obj): def issequence(obj):
if isinstance(obj, string_types): if isinstance(obj, string_types):
return False return False
@ -59,7 +54,16 @@ def specify(spec):
return spack.spec.Spec(spec) return spack.spec.Spec(spec)
class AspFunction(object): class AspObject(object):
"""Object representing a piece of ASP code."""
def _id(thing):
"""Quote string if needed for it to be a valid identifier."""
return thing if isinstance(thing, AspObject) else '"%s"' % str(thing)
class AspFunction(AspObject):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.args = [] self.args = []
@ -73,7 +77,7 @@ def __str__(self):
self.name, ', '.join(_id(arg) for arg in self.args)) self.name, ', '.join(_id(arg) for arg in self.args))
class AspAnd(object): class AspAnd(AspObject):
def __init__(self, *args): def __init__(self, *args):
args = listify(args) args = listify(args)
self.args = args self.args = args
@ -83,7 +87,7 @@ def __str__(self):
return s return s
class AspOr(object): class AspOr(AspObject):
def __init__(self, *args): def __init__(self, *args):
args = listify(args) args = listify(args)
self.args = args self.args = args
@ -92,7 +96,7 @@ def __str__(self):
return " | ".join(str(arg) for arg in self.args) return " | ".join(str(arg) for arg in self.args)
class AspNot(object): class AspNot(AspObject):
def __init__(self, arg): def __init__(self, arg):
self.arg = arg self.arg = arg
@ -100,6 +104,16 @@ def __str__(self):
return "not %s" % self.arg return "not %s" % self.arg
class AspOneOf(AspObject):
def __init__(self, *args):
args = listify(args)
self.args = args
def __str__(self):
body = "; ".join(str(arg) for arg in self.args)
return "1 { %s } 1" % body
class AspFunctionBuilder(object): class AspFunctionBuilder(object):
def __getattr__(self, name): def __getattr__(self, name):
return AspFunction(name) return AspFunction(name)
@ -112,6 +126,7 @@ class AspGenerator(object):
def __init__(self, out): def __init__(self, out):
self.out = out self.out = out
self.func = AspFunctionBuilder() self.func = AspFunctionBuilder()
self.possible_versions = {}
def title(self, name, char): def title(self, name, char):
self.out.write('\n') self.out.write('\n')
@ -133,6 +148,9 @@ def section(self, name):
self.out.write("%% %s\n" % name) self.out.write("%% %s\n" % name)
self.out.write("%\n") self.out.write("%\n")
def one_of(self, *args):
return AspOneOf(*args)
def _or(self, *args): def _or(self, *args):
return AspOr(*args) return AspOr(*args)
@ -158,31 +176,34 @@ def constraint(self, body):
self.out.write(":- %s.\n" % body) self.out.write(":- %s.\n" % body)
def pkg_version_rules(self, pkg): def pkg_version_rules(self, pkg):
"""Output declared versions of a package.
This uses self.possible_versions so that we include any versions
that arise from a spec.
"""
pkg = packagize(pkg) pkg = packagize(pkg)
for v in pkg.versions: for v in self.possible_versions[pkg.name]:
self.fact(fn.version_declared(pkg.name, v)) self.fact(fn.version_declared(pkg.name, v))
def spec_versions(self, spec): def spec_versions(self, spec):
"""Return list of clauses expressing spec's version constraints."""
spec = specify(spec) spec = specify(spec)
assert spec.name
if spec.concrete: if spec.concrete:
self.rule(fn.version(spec.name, spec.version), return [fn.version(spec.name, spec.version)]
fn.node(spec.name))
else:
versions = list(spec.package.versions)
# if the spec declares a new version, add it to the # version must be *one* of the ones the spec allows.
# possibilities. allowed_versions = [
if spec.versions.concrete and spec.version not in versions: v for v in self.possible_versions[spec.name]
self.fact(fn.version_declared(spec.name, spec.version)) if v.satisfies(spec.versions)
versions.append(spec.version) ]
predicates = [fn.version(spec.name, v) for v in allowed_versions]
# conflict with any versions that do not satisfy the spec # conflict with any versions that do not satisfy the spec
# TODO: need to traverse allspecs beforehand and ensure all if predicates:
# TODO: versions are known so we can disallow them. return [self.one_of(*predicates)]
for v in versions: return []
if not v.satisfies(spec.versions):
self.fact(fn.version_conflict(spec.name, v))
def compiler_defaults(self): def compiler_defaults(self):
"""Facts about available compilers.""" """Facts about available compilers."""
@ -259,30 +280,39 @@ def pkg_rules(self, pkg):
for cond, dep in conditions.items(): for cond, dep in conditions.items():
self.fact(fn.depends_on(dep.pkg.name, dep.spec.name)) self.fact(fn.depends_on(dep.pkg.name, dep.spec.name))
def spec_rules(self, spec): def spec_clauses(self, spec):
self.fact(fn.node(spec.name)) """Return a list of clauses the spec mandates are true.
self.spec_versions(spec)
Arguments:
spec (Spec): the spec to analyze
"""
clauses = []
if spec.name:
clauses.append(fn.node(spec.name))
clauses.extend(self.spec_versions(spec))
# seed architecture at the root (we'll propagate later) # seed architecture at the root (we'll propagate later)
# TODO: use better semantics. # TODO: use better semantics.
arch = spec.architecture arch = spec.architecture
if arch: if arch:
if arch.platform: if arch.platform:
self.fact(fn.arch_platform_set(spec.name, arch.platform)) clauses.append(fn.arch_platform_set(spec.name, arch.platform))
if arch.os: if arch.os:
self.fact(fn.arch_os_set(spec.name, arch.os)) clauses.append(fn.arch_os_set(spec.name, arch.os))
if arch.target: if arch.target:
self.fact(fn.arch_target_set(spec.name, arch.target)) clauses.append(fn.arch_target_set(spec.name, arch.target))
# variants # variants
for vname, variant in spec.variants.items(): for vname, variant in spec.variants.items():
self.fact(fn.variant_set(spec.name, vname, variant.value)) clauses.append(fn.variant_set(spec.name, vname, variant.value))
# compiler and compiler version # compiler and compiler version
if spec.compiler: if spec.compiler:
self.fact(fn.node_compiler_set(spec.name, spec.compiler.name)) clauses.append(fn.node_compiler_set(spec.name, spec.compiler.name))
if spec.compiler.concrete: if spec.compiler.concrete:
self.fact(fn.node_compiler_version_set( clauses.append(fn.node_compiler_version_set(
spec.name, spec.compiler.name, spec.compiler.version)) spec.name, spec.compiler.name, spec.compiler.version))
# TODO # TODO
@ -292,6 +322,22 @@ def spec_rules(self, spec):
# compiler_flags # compiler_flags
# namespace # namespace
return clauses
def build_version_dict(self, possible_pkgs, specs):
"""Declare any versions in specs not declared in packages."""
self.possible_versions = collections.defaultdict(lambda: set())
for pkg_name in possible_pkgs:
pkg = spack.repo.get(pkg_name)
for v in pkg.versions:
self.possible_versions[pkg_name].add(v)
for spec in specs:
for dep in spec.traverse():
if dep.versions.concrete:
self.possible_versions[dep.name].add(dep.version)
def arch_defaults(self): def arch_defaults(self):
"""Add facts about the default architecture for a package.""" """Add facts about the default architecture for a package."""
self.h2('Default architecture') self.h2('Default architecture')
@ -310,13 +356,20 @@ def generate_asp_program(self, specs):
""" """
# get list of all possible dependencies # get list of all possible dependencies
pkg_names = set(spec.fullname for spec in specs) pkg_names = set(spec.fullname for spec in specs)
pkgs = [spack.repo.path.get_pkg_class(name) for name in pkg_names]
pkgs = list(set(spack.package.possible_dependencies(*pkgs)) possible = set()
| set(pkg_names)) for name in pkg_names:
pkg = spack.repo.path.get_pkg_class(name)
possible.update(pkg.possible_dependencies())
pkgs = set(possible) | set(pkg_names)
concretize_lp = pkgutil.get_data('spack.solver', 'concretize.lp') concretize_lp = pkgutil.get_data('spack.solver', 'concretize.lp')
self.out.write(concretize_lp.decode("utf-8")) self.out.write(concretize_lp.decode("utf-8"))
# traverse all specs and packages to build dict of possible versions
self.build_version_dict(possible, specs)
self.h1('General Constraints') self.h1('General Constraints')
self.compiler_defaults() self.compiler_defaults()
self.arch_defaults() self.arch_defaults()
@ -330,7 +383,9 @@ def generate_asp_program(self, specs):
for spec in specs: for spec in specs:
for dep in spec.traverse(): for dep in spec.traverse():
self.h2('Spec: %s' % str(dep)) self.h2('Spec: %s' % str(dep))
self.spec_rules(dep) self.fact(fn.node(dep.name))
for clause in self.spec_clauses(dep):
self.rule(clause, fn.node(dep.name))
self.out.write('\n') self.out.write('\n')
display_lp = pkgutil.get_data('spack.solver', 'display.lp') display_lp = pkgutil.get_data('spack.solver', 'display.lp')