diff options
author | Todd Gamblin <tgamblin@llnl.gov> | 2021-01-02 20:28:46 -0800 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2021-05-22 11:51:20 -0700 |
commit | a823cffc40dda0738a9b8848c424efaa8a8ece2e (patch) | |
tree | 29472f915297f06dc1f5a055dd5d8186b3ba9dcb /lib | |
parent | f1c7402c64c0e3b7049517ee2d8f8be7fda9679b (diff) | |
download | spack-a823cffc40dda0738a9b8848c424efaa8a8ece2e.tar.gz spack-a823cffc40dda0738a9b8848c424efaa8a8ece2e.tar.bz2 spack-a823cffc40dda0738a9b8848c424efaa8a8ece2e.tar.xz spack-a823cffc40dda0738a9b8848c424efaa8a8ece2e.zip |
concretizer: unify logic for spec conditionals
This builds on #20638 by unifying all the places in the concretizer where
things are conditional on specs. Previously, we duplicated a common spec
conditional pattern for dependencies, virtual providers, conflicts, and
externals. That was introduced in #20423 and refined in #20507, and
roughly looked as follows.
Given some directives in a package like:
```python
depends_on("foo@1.0+bar", when="@2.0+variant")
provides("mpi@2:", when="@1.9:")
```
We handled the `@2.0+variant` and `@1.9:` parts by generating generated
`dependency_condition()`, `required_dependency_condition()`, and
`imposed_dependency_condition()` facts to trigger rules like this:
```prolog
dependency_conditions_hold(ID, Parent, Dependency) :-
attr(Name, Arg1) : required_dependency_condition(ID, Name, Arg1);
attr(Name, Arg1, Arg2) : required_dependency_condition(ID, Name, Arg1, Arg2);
attr(Name, Arg1, Arg2, Arg3) : required_dependency_condition(ID, Name, Arg1, Arg2, Arg3);
dependency_condition(ID, Parent, Dependency);
node(Parent).
```
And we handled `foo@1.0+bar` and `mpi@2:` parts ("imposed constraints")
like this:
```prolog
attr(Name, Arg1, Arg2) :-
dependency_conditions_hold(ID, Package, Dependency),
imposed_dependency_condition(ID, Name, Arg1, Arg2).
attr(Name, Arg1, Arg2, Arg3) :-
dependency_conditions_hold(ID, Package, Dependency),
imposed_dependency_condition(ID, Name, Arg1, Arg2, Arg3).
```
These rules were repeated with different input predicates for
requirements (e.g., `required_dependency_condition`) and imposed
constraints (e.g., `imposed_dependency_condition`) throughout
`concretize.lp`. In #20638 it got to be a bit confusing, because we used
the same `dependency_condition_holds` predicate to impose constraints on
conditional dependencies and virtual providers. So, even though the
pattern was repeated, some of the conditional rules were conjoined in a
weird way.
Instead of repeating this pattern everywhere, we now have *one* set of
consolidated rules for conditions:
```prolog
condition_holds(ID) :-
condition(ID);
attr(Name, A1) : condition_requirement(ID, Name, A1);
attr(Name, A1, A2) : condition_requirement(ID, Name, A1, A2);
attr(Name, A1, A2, A3) : condition_requirement(ID, Name, A1, A2, A3).
attr(Name, A1) :- condition_holds(ID), imposed_constraint(ID, Name, A1).
attr(Name, A1, A2) :- condition_holds(ID), imposed_constraint(ID, Name, A1, A2).
attr(Name, A1, A2, A3) :- condition_holds(ID), imposed_constraint(ID, Name, A1, A2, A3).
```
this allows us to use `condition(ID)` and `condition_holds(ID)` to
encapsulate the conditional logic on specs in all the scenarios where we
need it. Instead of defining predicates for the requirements and imposed
constraints, we generate the condition inputs with generic facts, and
define predicates to associate the condition ID with a particular
scenario. So, now, the generated facts for a condition look like this:
```prolog
condition(121).
condition_requirement(121,"node","cairo").
condition_requirement(121,"variant_value","cairo","fc","True").
imposed_constraint(121,"version_satisfies","fontconfig","2.10.91:").
dependency_condition(121,"cairo","fontconfig").
dependency_type(121,"build").
dependency_type(121,"link").
```
The requirements and imposed constraints are generic, and we associate
them with their meaning via the id. Here, `dependency_condition(121,
"cairo", "fontconfig")` tells us that condition 121 has to do with the
dependency of `cairo` on `fontconfig`, and the conditional dependency
rules just become:
```prolog
dependency_holds(Package, Dependency, Type) :-
dependency_condition(ID, Package, Dependency),
dependency_type(ID, Type),
condition_holds(ID).
```
Dependencies, virtuals, conflicts, and externals all now use similar
patterns, and the logic for generating condition facts is common to all
of them on the python side, as well. The more specific routines like
`package_dependencies_rules` just call `self.condition(...)` to get an id
and generate requirements and imposed constraints, then they generate
their extra facts with the returned id, like this:
```python
def package_dependencies_rules(self, pkg, tests):
"""Translate 'depends_on' directives into ASP logic."""
for _, conditions in sorted(pkg.dependencies.items()):
for cond, dep in sorted(conditions.items()):
condition_id = self.condition(cond, dep.spec, pkg.name) # create a condition and get its id
self.gen.fact(fn.dependency_condition( # associate specifics about the dependency w/the id
condition_id, pkg.name, dep.spec.name
))
# etc.
```
- [x] unify generation and logic for conditions
- [x] use unified logic for dependencies
- [x] use unified logic for virtuals
- [x] use unified logic for conflicts
- [x] use unified logic for externals
LocalWords: concretizer mpi attr Arg concretize lp cairo fc fontconfig
LocalWords: virtuals def pkg cond dep fn refactor github py
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/spack/solver/asp.py | 130 | ||||
-rw-r--r-- | lib/spack/spack/solver/concretize.lp | 170 | ||||
-rw-r--r-- | lib/spack/spack/solver/display.lp | 2 |
3 files changed, 119 insertions, 183 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index 8d2b2b8aa5..61d18fc65e 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -483,28 +483,12 @@ class SpackSolverSetup(object): def conflict_rules(self, pkg): for trigger, constraints in pkg.conflicts.items(): + trigger_id = self.condition(spack.spec.Spec(trigger), name=pkg.name) + self.gen.fact(fn.conflict_trigger(trigger_id)) + 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'] - clauses = [x for x in clauses if x.name not in to_be_filtered] - - # Emit facts based on clauses - condition_id = next(self._condition_id_counter) - self.gen.fact(fn.conflict(condition_id, pkg.name)) - for clause in clauses: - self.gen.fact(fn.conflict_condition( - condition_id, clause.name, *clause.args - )) + constraint_id = self.condition(constraint, name=pkg.name) + self.gen.fact(fn.conflict(pkg.name, trigger_id, constraint_id)) self.gen.newline() def available_compilers(self): @@ -642,50 +626,44 @@ class SpackSolverSetup(object): ) ) - def _condition_facts( - self, pkg_name, cond_spec, dep_spec, - cond_fn, require_fn, impose_fn - ): + def condition(self, required_spec, imposed_spec=None, name=None): """Generate facts for a dependency or virtual provider condition. Arguments: - pkg_name (str): name of the package that triggers the - condition (e.g., the dependent or the provider) - cond_spec (Spec): the dependency spec representing the - condition that needs to be True (can be anonymous) - dep_spec (Spec): the sepc of the dependency or provider - to be depended on/provided if the condition holds. - cond_fn (AspFunction): function to use to declare the condition; - will be called with the cond id, pkg_name, an dep_spec.name - require_fn (AspFunction): function to use to declare the conditions - required of the dependent/provider to trigger - impose_fn (AspFunction): function to use for constraints imposed - on the dependency/virtual + required_spec (Spec): the spec that triggers this condition + imposed_spec (optional, Spec): the sepc with constraints that + are imposed when this condition is triggered + name (optional, str): name for `required_spec` (required if + required_spec is anonymous, ignored if not) Returns: (int): id of the condition created by this function """ - condition_id = next(self._condition_id_counter) - named_cond = cond_spec.copy() - named_cond.name = named_cond.name or pkg_name + named_cond = required_spec.copy() + named_cond.name = named_cond.name or name + assert named_cond.name, "must provide name for anonymous condtions!" - self.gen.fact(cond_fn(condition_id, pkg_name, dep_spec.name)) + condition_id = next(self._condition_id_counter) + self.gen.fact(fn.condition(condition_id)) - # conditions that trigger the condition - conditions = self.checked_spec_clauses( - named_cond, body=True, required_from=pkg_name - ) - for pred in conditions: - self.gen.fact(require_fn(condition_id, pred.name, *pred.args)) + # requirements trigger the condition + requirements = self.checked_spec_clauses( + named_cond, body=True, required_from=name) + for pred in requirements: + self.gen.fact( + fn.condition_requirement(condition_id, pred.name, *pred.args) + ) - imposed_constraints = self.checked_spec_clauses( - dep_spec, required_from=pkg_name - ) - for pred in imposed_constraints: - # imposed "node"-like conditions are no-ops - if pred.name in ("node", "virtual_node"): - continue - self.gen.fact(impose_fn(condition_id, pred.name, *pred.args)) + if imposed_spec: + imposed_constraints = self.checked_spec_clauses( + imposed_spec, body=False, required_from=name) + for pred in imposed_constraints: + # imposed "node"-like conditions are no-ops + if pred.name in ("node", "virtual_node"): + continue + self.gen.fact( + fn.imposed_constraint(condition_id, pred.name, *pred.args) + ) return condition_id @@ -695,25 +673,20 @@ class SpackSolverSetup(object): for provided, whens in pkg.provided.items(): for when in whens: - self._condition_facts( - pkg.name, when, provided, - fn.provider_condition, - fn.required_provider_condition, - fn.imposed_dependency_condition - ) - + condition_id = self.condition(when, provided, pkg.name) + self.gen.fact(fn.provider_condition( + condition_id, when.name, provided.name + )) self.gen.newline() def package_dependencies_rules(self, pkg, tests): """Translate 'depends_on' directives into ASP logic.""" for _, conditions in sorted(pkg.dependencies.items()): for cond, dep in sorted(conditions.items()): - condition_id = self._condition_facts( - pkg.name, cond, dep.spec, - fn.dependency_condition, - fn.required_dependency_condition, - fn.imposed_dependency_condition - ) + condition_id = self.condition(cond, dep.spec, pkg.name) + self.gen.fact(fn.dependency_condition( + condition_id, pkg.name, dep.spec.name + )) for t in sorted(dep.type): # Skip test dependencies if they're not requested at all @@ -794,24 +767,13 @@ class SpackSolverSetup(object): pkg_name, str(version), weight, id )) + # Declare external conditions with a local index into packages.yaml for local_idx, spec in enumerate(external_specs): - condition_id = next(self._condition_id_counter) - - # Declare the global ID associated with this external spec - self.gen.fact(fn.external_spec(condition_id, pkg_name)) - - # Local index into packages.yaml + condition_id = self.condition(spec) self.gen.fact( - fn.external_spec_index(condition_id, pkg_name, local_idx)) - - # Add conditions to be satisfied for this external + fn.possible_external(condition_id, pkg_name, local_idx) + ) self.possible_versions[spec.name].add(spec.version) - clauses = self.spec_clauses(spec, body=True) - for clause in clauses: - self.gen.fact( - fn.external_spec_condition( - condition_id, clause.name, *clause.args) - ) self.gen.newline() def preferred_variants(self, pkg_name): @@ -1474,7 +1436,7 @@ class SpecBuilder(object): def no_flags(self, pkg, flag_type): self._specs[pkg].compiler_flags[flag_type] = [] - def external_spec_selected(self, condition_id, pkg, idx): + def external_spec_selected(self, pkg, idx): """This means that the external spec and index idx has been selected for this package. """ diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp index d0174ca2e0..01e70b9469 100644 --- a/lib/spack/spack/solver/concretize.lp +++ b/lib/spack/spack/solver/concretize.lp @@ -32,15 +32,53 @@ version_satisfies(Package, Constraint) #defined version_satisfies/3. %----------------------------------------------------------------------------- +% Spec conditions and imposed constraints +% +% Given Spack directives like these: +% depends_on("foo@1.0+bar", when="@2.0+variant") +% provides("mpi@2:", when="@1.9:") +% +% The conditions are `@2.0+variant` and `@1.9:`, and the imposed constraints +% are `@1.0+bar` on `foo` and `@2:` on `mpi`. +%----------------------------------------------------------------------------- +% conditions are specified with `condition_requirement` and hold when +% corresponding spec attributes hold. +condition_holds(ID) :- + condition(ID); + attr(Name, A1) : condition_requirement(ID, Name, A1); + attr(Name, A1, A2) : condition_requirement(ID, Name, A1, A2); + attr(Name, A1, A2, A3) : condition_requirement(ID, Name, A1, A2, A3). + +% conditions that hold impose constraints on other specs +attr(Name, A1) :- condition_holds(ID), imposed_constraint(ID, Name, A1). +attr(Name, A1, A2) :- condition_holds(ID), imposed_constraint(ID, Name, A1, A2). +attr(Name, A1, A2, A3) :- condition_holds(ID), imposed_constraint(ID, Name, A1, A2, A3). + +#defined condition/1. +#defined condition_requirement/3. +#defined condition_requirement/4. +#defined condition_requirement/5. +#defined imposed_constraint/3. +#defined imposed_constraint/4. +#defined imposed_constraint/5. + +%----------------------------------------------------------------------------- % Dependency semantics %----------------------------------------------------------------------------- % Dependencies of any type imply that one package "depends on" another depends_on(Package, Dependency) :- depends_on(Package, Dependency, _). +% a dependency holds if its condition holds +dependency_holds(Package, Dependency, Type) :- + dependency_condition(ID, Package, Dependency), + dependency_type(ID, Type), + condition_holds(ID). + % declared dependencies are real if they're not virtual AND -% the package is not an external +% the package is not an external. +% They're only triggered if the associated dependnecy condition holds. depends_on(Package, Dependency, Type) - :- dependency_conditions(Package, Dependency, Type), + :- dependency_holds(Package, Dependency, Type), not virtual(Dependency), not external(Package). @@ -64,74 +102,19 @@ path(Parent, Child) :- depends_on(Parent, Child). path(Parent, Descendant) :- path(Parent, A), depends_on(A, Descendant). :- path(A, B), path(B, A). -%----------------------------------------------------------------------------- -% Conditional dependencies -% -% This takes care of `when=SPEC` in `depends_on("foo@1.0+bar", when="SPEC")`. -%----------------------------------------------------------------------------- -% if any individual condition below is true, trigger the dependency. -dependency_conditions(Package, Dependency, Type) :- - dependency_conditions_hold(ID, Package, Dependency), - dependency_type(ID, Type). - #defined dependency_type/2. - -% collect all the dependency conditions into a single conditional rule -% distinguishing between Parent and Package (Arg1) is needed to account -% for conditions like: -% -% depends_on('patchelf@0.9', when='@1.0:1.1 ^python@:2') -% -% that include dependencies -dependency_conditions_hold(ID, Parent, Dependency) :- - attr(Name, Arg1) : required_dependency_condition(ID, Name, Arg1); - attr(Name, Arg1, Arg2) : required_dependency_condition(ID, Name, Arg1, Arg2); - attr(Name, Arg1, Arg2, Arg3) : required_dependency_condition(ID, Name, Arg1, Arg2, Arg3); - dependency_condition(ID, Parent, Dependency); - % There must be at least a dependency type declared, - % otherwise the dependency doesn't hold - dependency_type(ID, _); - node(Parent); - not external(Parent). - #defined dependency_condition/3. -#defined required_dependency_condition/3. -#defined required_dependency_condition/4. -#defined required_dependency_condition/5. - -%----------------------------------------------------------------------------- -% Imposed constraints on dependencies -% -% This handles the `@1.0+bar` in `depends_on("foo@1.0+bar", when="SPEC")`, or -% the `mpi@2:` in `provides("mpi@2:", when="@1.9:")`. -%----------------------------------------------------------------------------- -% NOTE: `attr(Name, Arg1)` is omitted here b/c the only single-arg attribute is -% NOTE: `node()`, which is handled above under "Dependency Semantics" - -attr(Name, Arg1, Arg2) :- - dependency_conditions_hold(ID, Package, Dependency), - imposed_dependency_condition(ID, Name, Arg1, Arg2). - -attr(Name, Arg1, Arg2, Arg3) :- - dependency_conditions_hold(ID, Package, Dependency), - imposed_dependency_condition(ID, Name, Arg1, Arg2, Arg3). - -#defined imposed_dependency_condition/4. -#defined imposed_dependency_condition/5. %----------------------------------------------------------------------------- % Conflicts %----------------------------------------------------------------------------- -:- not external(Package) : conflict_condition(ID, "node", Package); - attr(Name, Arg1) : conflict_condition(ID, Name, Arg1); - attr(Name, Arg1, Arg2) : conflict_condition(ID, Name, Arg1, Arg2); - attr(Name, Arg1, Arg2, Arg3) : conflict_condition(ID, Name, Arg1, Arg2, Arg3); - conflict(ID, Package). +:- node(Package), + not external(Package), + conflict(Package, TriggerID, ConstraintID), + condition_holds(TriggerID), + condition_holds(ConstraintID). -#defined conflict/2. -#defined conflict_condition/3. -#defined conflict_condition/4. -#defined conflict_condition/5. +#defined conflict/3. %----------------------------------------------------------------------------- % Virtual dependencies @@ -140,10 +123,15 @@ attr(Name, Arg1, Arg2, Arg3) :- % if a package depends on a virtual, it's not external and we have a % provider for that virtual then it depends on the provider depends_on(Package, Provider, Type) - :- dependency_conditions(Package, Virtual, Type), + :- dependency_holds(Package, Virtual, Type), provides_virtual(Provider, Virtual), not external(Package). +% dependencies on virtuals also imply that the virtual is a virtual node +virtual_node(Virtual) + :- dependency_holds(Package, Virtual, Type), + virtual(Virtual), not external(Package). + % if there's a virtual node, we must select one provider 1 { provides_virtual(Package, Virtual) : possible_provider(Package, Virtual) } 1 :- virtual_node(Virtual). @@ -153,32 +141,21 @@ virtual_node(Virtual) :- virtual_root(Virtual). 1 { root(Package) : provides_virtual(Package, Virtual) } 1 :- virtual_root(Virtual). -% all virtual providers come from provider conditions like this -dependency_conditions_hold(ID, Provider, Virtual) :- - attr(Name, Arg1) : required_provider_condition(ID, Name, Arg1); - attr(Name, Arg1, Arg2) : required_provider_condition(ID, Name, Arg1, Arg2); - attr(Name, Arg1, Arg2, Arg3) : required_provider_condition(ID, Name, Arg1, Arg2, Arg3); - virtual(Virtual); - provider_condition(ID, Provider, Virtual). - % The provider provides the virtual if some provider condition holds. provides_virtual(Provider, Virtual) :- provider_condition(ID, Provider, Virtual), - dependency_conditions_hold(ID, Provider, Virtual), + condition_holds(ID), virtual(Virtual). % a node that provides a virtual is a provider provider(Package, Virtual) :- node(Package), provides_virtual(Package, Virtual). -% dependencies on virtuals also imply that the virtual is a virtual node -virtual_node(Virtual) - :- dependency_conditions(Package, Virtual, Type), - virtual(Virtual), not external(Package). - % for any virtual, there can be at most one provider in the DAG 0 { node(Package) : provides_virtual(Package, Virtual) } 1 :- virtual(Virtual). +#defined possible_provider/2. + %----------------------------------------------------------------------------- % Virtual dependency weights %----------------------------------------------------------------------------- @@ -302,32 +279,29 @@ external(Package) :- external_only(Package), node(Package). % a package is a real_node if it is not external real_node(Package) :- node(Package), not external(Package). -% if an external version is selected, the package is external and -% we are using the corresponding spec -external(Package) :- - version(Package, Version), version_weight(Package, Weight), - external_version_declared(Package, Version, Weight, ID). +% a package is external if we are using an external spec for it +external(Package) :- external_spec_selected(Package, _). + +% we can't use the weight for an external version if we don't use the +% corresponding external spec. +:- version(Package, Version), + version_weight(Package, Weight), + external_version_declared(Package, Version, Weight, ID), + not external(Package). % determine if an external spec has been selected -external_spec_selected(ID, Package, LocalIndex) :- - version(Package, Version), version_weight(Package, Weight), - external_spec_index(ID, Package, LocalIndex), - external_version_declared(Package, Version, Weight, LocalIndex), - external_spec_conditions_hold(ID, Package). - -% determine if all the conditions on an external spec hold. If they do -% the spec can be selected. -external_spec_conditions_hold(ID, Package) :- - attr(Name, Arg1) : external_spec_condition(ID, Name, Arg1); - attr(Name, Arg1, Arg2) : external_spec_condition(ID, Name, Arg1, Arg2); - attr(Name, Arg1, Arg2, Arg3) : external_spec_condition(ID, Name, Arg1, Arg2, Arg3); - external_spec(ID, Package); - node(Package). +external_spec_selected(Package, LocalIndex) :- + external_conditions_hold(Package, LocalIndex), + node(Package). + +external_conditions_hold(Package, LocalIndex) :- + possible_external(ID, Package, LocalIndex), condition_holds(ID). % it cannot happen that a spec is external, but none of the external specs % conditions hold. -:- external(Package), not external_spec_conditions_hold(_, Package). +:- external(Package), not external_conditions_hold(Package, _). +#defined possible_external/3. #defined external_spec_index/3. #defined external_spec_condition/3. #defined external_spec_condition/4. diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp index 4b862a26e2..fdc4e4088c 100644 --- a/lib/spack/spack/solver/display.lp +++ b/lib/spack/spack/solver/display.lp @@ -24,4 +24,4 @@ #show compiler_weight/2. #show node_target_match/2. #show node_target_weight/2. -#show external_spec_selected/3. +#show external_spec_selected/2. |