summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2023-06-19 20:23:59 +0200
committerTodd Gamblin <tgamblin@llnl.gov>2023-08-15 15:54:37 -0700
commit22c95923e3ec876cb37317ce58ef895ef5ddf4ce (patch)
treefba43a6249fe18755429639e027c8d4306d22847 /lib
parentc050b99a06a3ad11c76962b08141e513ea69b6c1 (diff)
downloadspack-22c95923e3ec876cb37317ce58ef895ef5ddf4ce.tar.gz
spack-22c95923e3ec876cb37317ce58ef895ef5ddf4ce.tar.bz2
spack-22c95923e3ec876cb37317ce58ef895ef5ddf4ce.tar.xz
spack-22c95923e3ec876cb37317ce58ef895ef5ddf4ce.zip
Parametrize all the logic program for multiple nodes
Introduce the concept of "condition sets", i.e. the set of packages on which a package can require / impose conditions. This currently maps to the link/run sub-dag of each package + its direct build dependencies. Parametrize the "condition" and "requirement" logic to multiple nodes.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/solver/asp.py5
-rw-r--r--lib/spack/spack/solver/concretize.lp229
-rw-r--r--lib/spack/spack/solver/heuristic.lp28
3 files changed, 185 insertions, 77 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 616c1740c9..1928b22150 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -776,6 +776,7 @@ class PyclingoDriver:
# Load the file itself
self.control.load(os.path.join(parent_dir, "concretize.lp"))
+ self.control.load(os.path.join(parent_dir, "heuristic.lp"))
self.control.load(os.path.join(parent_dir, "os_compatibility.lp"))
self.control.load(os.path.join(parent_dir, "display.lp"))
timer.stop("load")
@@ -1284,9 +1285,13 @@ class SpackSolverSetup:
def package_provider_rules(self, pkg):
for provider_name in sorted(set(s.name for s in pkg.provided.keys())):
+ if provider_name not in self.possible_virtuals:
+ continue
self.gen.fact(fn.facts(pkg.name, fn.possible_provider(provider_name)))
for provided, whens in pkg.provided.items():
+ if provided.name not in self.possible_virtuals:
+ continue
for when in whens:
msg = "%s provides %s when %s" % (pkg.name, provided, when)
condition_id = self.condition(when, provided, pkg.name, msg)
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 77fa3d47be..3268f581f7 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -13,6 +13,9 @@
#const root_node_id = 0.
+#const link_run = 0.
+#const direct_build = 1.
+
% Allow clingo to create nodes
{ attr("node", node(0..X-1, Package)) } :- max_nodes(Package, X), not virtual(Package).
@@ -39,12 +42,42 @@ max_nodes(Package, 1) :- virtual(Package).
:- attr("node_flag_source", ParentNode, _, _), not attr("node", ParentNode).
:- attr("node_flag_source", _, _, ChildNode), not attr("node", ChildNode).
+:- attr("virtual_node", VirtualNode), not provider(_, VirtualNode).
+:- provider(_, VirtualNode), not attr("virtual_node", VirtualNode).
+:- provider(PackageNode, _), not attr("node", PackageNode).
+
+:- attr("root", node(ID, PackageNode)), ID> root_node_id.
+
+% Root nodes cannot depend on non-root nodes if the dependency is "link" or "run"
+:- attr("depends_on", node(root_node_id, _), node(ID, _), "link"), ID != root_node_id.
+:- attr("depends_on", node(root_node_id, _), node(ID, _), "run"), ID != root_node_id.
+
% Rules on "unification sets", i.e. on sets of nodes allowing a single configuration of any given package
-unification_set("root", PackageNode) :- attr("root", PackageNode).
-unification_set(SetID, ChildNode) :- attr("depends_on", ParentNode, ChildNode, _), unification_set(SetID, ParentNode).
unify(SetID, PackageName) :- unification_set(SetID, node(_, PackageName)).
:- 2 { unification_set(SetID, node(_, PackageName)) }, unify(SetID, PackageName).
+unification_set("root", PackageNode) :- attr("root", PackageNode).
+unification_set(SetID, ChildNode) :- attr("depends_on", ParentNode, ChildNode, Type), Type != "build", unification_set(SetID, ParentNode).
+unification_set(("build", ParentNode), ChildNode) :- attr("depends_on", ParentNode, ChildNode, Type), Type == "build", unification_set("root", ParentNode).
+unification_set(SetID, ChildNode) :- attr("depends_on", ParentNode, ChildNode, Type), Type == "build", SetID != "root", unification_set(SetID, ParentNode).
+unification_set(SetID, VirtualNode) :- provider(PackageNode, VirtualNode), unification_set(SetID, PackageNode).
+
+%----
+% Rules to break symmetry and speed-up searches
+%----
+
+% In the "root" unification set only ID = 0 are allowed
+:- unification_set("root", node(ID, _)), ID != 0.
+
+% Cannot have a node with an ID, if lower ID of the same package are not used
+:- attr("node", node(ID1, Package)),
+ not attr("node", node(ID2, Package)),
+ max_nodes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1.
+
+:- attr("virtual_node", node(ID1, Package)),
+ not attr("virtual_node", node(ID2, Package)),
+ max_nodes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1.
+
% Give clingo the choice to solve an input spec or not
{ literal_solved(ID) } :- literal(ID).
literal_not_solved(ID) :- not literal_solved(ID), literal(ID).
@@ -63,15 +96,16 @@ opt_criterion(300, "number of input specs not concretized").
% TODO: literals, at the moment, can only influence the "root" unification set. This needs to be extended later.
+special_case("node_flag_source").
+special_case("depends_on").
+
% Map constraint on the literal ID to facts on the node
attr(Name, node(root_node_id, A1)) :- literal(LiteralID, Name, A1), literal_solved(LiteralID).
attr(Name, node(root_node_id, A1), A2) :- literal(LiteralID, Name, A1, A2), literal_solved(LiteralID).
-attr(Name, node(root_node_id, A1), A2, A3) :- literal(LiteralID, Name, A1, A2, A3), literal_solved(LiteralID), not literal_special_case(LiteralID, Name, A1, A2, A3).
+attr(Name, node(root_node_id, A1), A2, A3) :- literal(LiteralID, Name, A1, A2, A3), literal_solved(LiteralID), not special_case(Name).
attr(Name, node(root_node_id, A1), A2, A3, A4) :- literal(LiteralID, Name, A1, A2, A3, A4), literal_solved(LiteralID).
% Special cases where nodes occur in arguments other than A1
-literal_special_case(LiteralID, Name, A1, A2, A3) :- literal(LiteralID, Name, A1, A2, A3), Name == "node_flag_source".
-literal_special_case(LiteralID, Name, A1, A2, A3) :- literal(LiteralID, Name, A1, A2, A3), Name == "depends_on".
attr("node_flag_source", node(root_node_id, A1), A2, node(root_node_id, A3)) :- literal(LiteralID, "node_flag_source", A1, A2, A3), literal_solved(LiteralID).
attr("depends_on", node(root_node_id, A1), node(root_node_id, A2), A3) :- literal(LiteralID, "depends_on", A1, A2, A3), literal_solved(LiteralID).
@@ -213,30 +247,100 @@ attr("node_version_satisfies", node(ID, Package), Constraint)
%-----------------------------------------------------------------------------
% conditions are specified with `condition_requirement` and hold when
% corresponding spec attributes hold.
-condition_holds(ID, node(root_node_id, Package)) :-
- facts(Package, condition(ID));
- attr(Name, node(root_node_id, A1)) : condition_requirement(ID, Name, A1);
- attr(Name, node(root_node_id, A1), A2) : condition_requirement(ID, Name, A1, A2);
- attr(Name, node(root_node_id, A1), A2, A3) : condition_requirement(ID, Name, A1, A2, A3), Name != "node_flag_source";
- attr(Name, node(root_node_id, A1), A2, A3, A4) : condition_requirement(ID, Name, A1, A2, A3, A4);
+
+% A "condition_set(PackageNode, _)" is the set of nodes on which PackageNode can require / impose conditions
+% Currently, for a given node, this is the link-run sub-DAG of PackageNode and its direct build dependencies
+condition_set(PackageNode, PackageNode, link_run) :- attr("node", PackageNode).
+
+condition_set(PackageNode, PackageNode, link_run) :- provider(PackageNode, VirtualNode).
+condition_set(PackageNode, VirtualNode, link_run) :- provider(PackageNode, VirtualNode).
+
+:- condition_set(node(root_node_id, Package), node(ID, A1), link_run), ID > root_node_id.
+
+condition_set(ID, DependencyNode, link_run)
+ :- condition_set(ID, PackageNode, link_run),
+ attr("depends_on", PackageNode, DependencyNode, Type),
+ Type != "build".
+
+condition_set(PackageNode, DependencyNode, direct_build) :- condition_set(PackageNode, PackageNode, link_run), attr("depends_on", PackageNode, DependencyNode, "build").
+condition_set(ID, VirtualNode, Type) :- condition_set(ID, PackageNode, Type), provider(PackageNode, VirtualNode).
+
+condition_set(ID, PackageNode) :- condition_set(ID, PackageNode, _).
+
+condition_packages(ID, A1) :- condition_requirement(ID, _, A1).
+condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _).
+condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _, _).
+condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _, _, _).
+
+node_condition(ID, node(PackageID, Package)) :- facts(Package, condition(ID)), attr("node", node(PackageID, Package)).
+node_condition(ID, node(PackageID, Package)) :- facts(Virtual, condition(ID)), provider(node(PackageID, Package), node(_, Virtual)).
+
+condition_nodes(ConditionID, PackageNode, node(X, A1))
+ :- condition_packages(ConditionID, A1),
+ condition_set(PackageNode, node(X, A1)),
+ node_condition(ConditionID, PackageNode).
+
+cannot_hold(ConditionID, PackageNode)
+ :- condition_packages(ConditionID, A1),
+ not condition_set(PackageNode, node(_, A1), _),
+ node_condition(ConditionID, PackageNode).
+
+condition_holds(ID, PackageNode) :-
+ node_condition(ID, PackageNode);
+ attr(Name, node(X, A1)) : condition_requirement(ID, Name, A1), condition_nodes(ID, PackageNode, node(X, A1));
+ attr(Name, node(X, A1), A2) : condition_requirement(ID, Name, A1, A2), condition_nodes(ID, PackageNode, node(X, A1));
+ attr(Name, node(X, A1), A2, A3) : condition_requirement(ID, Name, A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), not special_case(Name);
+ attr(Name, node(X, A1), A2, A3, A4) : condition_requirement(ID, Name, A1, A2, A3, A4), condition_nodes(ID, PackageNode, node(X, A1));
% Special cases
- attr("node_flag_source", node(root_node_id, Package), A2, node(root_node_id, A3)) : condition_requirement(ID, "node_flag_source", Package, A2, A3).
+ attr("node_flag_source", node(X, A1), A2, node(Y, A3)) : condition_requirement(ID, "node_flag_source", A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), condition_nodes(ID, PackageNode, node(Y, A3));
+ not cannot_hold(ID, PackageNode).
+
+condition_holds(ID, node(VirtualID, Virtual))
+ :- condition_holds(ID, PackageNode),
+ facts(Virtual, condition(ID)),
+ provider(PackageNode, node(VirtualID, Virtual)).
% condition_holds(ID, node(ID, Package)) implies all imposed_constraints, unless do_not_impose(ID, node(ID, Package))
% is derived. This allows imposed constraints to be canceled in special cases.
-impose(ID, PackageNode) :- condition_holds(ID, PackageNode), not do_not_impose(ID, PackageNode).
+impose(ID, PackageNode) :- condition_holds(ID, PackageNode), node_condition(ID, PackageNode), not do_not_impose(ID, PackageNode).
-% Conditions that hold impose may impose constraints on other specs
-attr(Name, node(root_node_id, A1)) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, Name, A1).
-attr(Name, node(root_node_id, A1), A2) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, Name, A1, A2).
-attr(Name, node(root_node_id, A1), A2, A3) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, Name, A1, A2, A3), not special_case(ID, Name, A1, A2, A3).
-attr(Name, node(root_node_id, A1), A2, A3, A4) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, Name, A1, A2, A3, A4).
+imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1).
+imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _).
+imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _, _).
+imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _, _, _).
+imposed_packages(ID, A1) :- imposed_constraint(ID, "depends_on", _, A1, _).
-% Special cases
-special_case(ID, Name, A1, A2, A3) :- imposed_constraint(ID, Name, A1, A2, A3), Name == "node_flag_source".
-special_case(ID, Name, A1, A2, A3) :- imposed_constraint(ID, Name, A1, A2, A3), Name == "depends_on".
-attr("node_flag_source", node(NodeID, Package), A2, node(0, A3)) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, "node_flag_source", Package, A2, A3).
-attr("depends_on", node(NodeID, Package), node(0, A2), A3) :- impose(ID, node(NodeID, Package)), imposed_constraint(ID, "depends_on", Package, A2, A3).
+imposed_nodes(ConditionID, PackageNode, node(X, A1))
+ :- imposed_packages(ConditionID, A1),
+ condition_set(PackageNode, node(X, A1), _),
+ node_condition(ConditionID, PackageNode).
+
+imposed_nodes(ConditionID, PackageNode, node(X, A1))
+ :- imposed_packages(ConditionID, A1),
+ condition_set(PackageNode, node(X, A1), _),
+ attr("hash", PackageNode, ConditionID).
+
+:- imposed_packages(ID, A1), impose(ID, PackageNode), not condition_set(PackageNode, node(_, A1)).
+
+% Conditions that hold impose may impose constraints on other specs
+attr(Name, node(X, A1)) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1), imposed_nodes(ID, PackageNode, node(X, A1)).
+attr(Name, node(X, A1), A2) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2), imposed_nodes(ID, PackageNode, node(X, A1)).
+attr(Name, node(X, A1), A2, A3) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3), imposed_nodes(ID, PackageNode, node(X, A1)), not special_case(Name).
+attr(Name, node(X, A1), A2, A3, A4) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3, A4), imposed_nodes(ID, PackageNode, node(X, A1)).
+
+% For node flag sources we need to look at the condition_set of the source, since it is the dependent
+% of the package on which I want to impose the constraint
+attr("node_flag_source", node(X, A1), A2, node(Y, A3))
+ :- impose(ID, node(X, A1)),
+ imposed_constraint(ID, "node_flag_source", A1, A2, A3),
+ condition_set(node(Y, A3), node(X, A1)).
+
+% Here we can't use the condition set because it's a recursive definition, that doesn't define the
+% node index, and leads to unsatisfiability. Hence we say that one and only one node index must
+% satisfy the dependency.
+1 { attr("depends_on", node(X, A1), node(0..Y-1, A2), A3) : max_nodes(A2, Y) } 1
+ :- impose(ID, node(X, A1)),
+ imposed_constraint(ID, "depends_on", A1, A2, A3).
% we cannot have additional variant values when we are working with concrete specs
:- attr("node", node(ID, Package)),
@@ -336,26 +440,22 @@ error(1, Msg)
% provider for that virtual then it depends on the provider
attr("depends_on", PackageNode, ProviderNode, Type)
:- dependency_holds(PackageNode, Virtual, Type),
- provider(ProviderNode, node(0, Virtual)),
+ provider(ProviderNode, node(VirtualID, Virtual)),
not external(PackageNode).
attr("virtual_on_edge", PackageNode, ProviderNode, Virtual)
:- dependency_holds(PackageNode, Virtual, Type),
- provider(ProviderNode, node(0, Virtual)),
+ provider(ProviderNode, node(VirtualID, Virtual)),
not external(PackageNode).
% dependencies on virtuals also imply that the virtual is a virtual node
-attr("virtual_node", node(0, Virtual))
+1 { attr("virtual_node", node(0..X-1, Virtual)) : max_nodes(Virtual, X) }
:- dependency_holds(PackageNode, Virtual, Type),
virtual(Virtual), not external(PackageNode).
% If there's a virtual node, we must select one and only one provider.
% The provider must be selected among the possible providers.
-{ provider(node(0..X-1, Package), node(VirtualID, Virtual))
- : facts(Package, possible_provider(Virtual)), max_nodes(Package, X) }
- :- attr("virtual_node", node(VirtualID, Virtual)).
-
error(100, "Cannot find valid provider for virtual {0}", VirtualNode)
:- attr("virtual_node", VirtualNode),
not provider(_, VirtualNode).
@@ -376,10 +476,11 @@ attr("root", PackageNode) :- attr("virtual_root", VirtualNode), provider(Package
% for environments that are concretized together (e.g. where we
% asks to install "mpich" and "hdf5+mpi" and we want "mpich" to
% be the mpi provider)
-provider(PackageNode, node(0, Virtual)) :- attr("node", PackageNode), virtual_condition_holds(PackageNode, Virtual).
+1 { provider(PackageNode, node(0..X-1, Virtual)) : max_nodes(Virtual, X) } 1 :- attr("node", PackageNode), virtual_condition_holds(PackageNode, Virtual).
+:- 2 { provider(PackageNode, VirtualNode) }, attr("virtual_node", VirtualNode).
% The provider provides the virtual if some provider condition holds.
-virtual_condition_holds(node(ProviderID, Provider), Virtual) :-virtual_condition_holds(ID, node(ProviderID, Provider), Virtual).
+virtual_condition_holds(node(ProviderID, Provider), Virtual) :- virtual_condition_holds(ID, node(ProviderID, Provider), Virtual).
virtual_condition_holds(ID, node(ProviderID, Provider), Virtual) :-
facts(Provider, provider_condition(ID, Virtual)),
condition_holds(ID, node(ProviderID, Provider)),
@@ -387,7 +488,7 @@ virtual_condition_holds(ID, node(ProviderID, Provider), Virtual) :-
% A package cannot be the actual provider for a virtual if it does not
% fulfill the conditions to provide that virtual
-:- provider(PackageNode, node(0, Virtual)),
+:- provider(PackageNode, node(VirtualID, Virtual)),
not virtual_condition_holds(PackageNode, Virtual),
internal_error("Virtual when provides not respected").
@@ -398,13 +499,10 @@ virtual_condition_holds(ID, node(ProviderID, Provider), Virtual) :-
% A provider may have different possible weights depending on whether it's an external
% or not, or on preferences expressed in packages.yaml etc. This rule ensures that
% we select the weight, among the possible ones, that minimizes the overall objective function.
-1 { provider_weight(DependencyNode, VirtualNode, Weight, Reason) :
- possible_provider_weight(DependencyNode, VirtualNode, Weight, Reason) } 1
+1 { provider_weight(DependencyNode, VirtualNode, Weight) :
+ possible_provider_weight(DependencyNode, VirtualNode, Weight, _) } 1
:- provider(DependencyNode, VirtualNode), internal_error("Package provider weights must be unique").
-% Get rid or the reason for enabling the possible weight (useful for debugging)
-provider_weight(DependencyNode, VirtualNode, Weight) :- provider_weight(DependencyNode, VirtualNode, Weight, _).
-
% A provider that is an external can use a weight of 0
possible_provider_weight(DependencyNode, VirtualNode, 0, "external")
:- provider(DependencyNode, VirtualNode),
@@ -412,15 +510,15 @@ possible_provider_weight(DependencyNode, VirtualNode, 0, "external")
% A provider mentioned in packages.yaml can use a weight
% according to its priority in the list of providers
-possible_provider_weight(node(DependencyID, Dependency), node(0, Virtual), Weight, "packages_yaml")
- :- provider(node(DependencyID, Dependency), node(0, Virtual)),
+possible_provider_weight(node(DependencyID, Dependency), node(VirtualID, Virtual), Weight, "packages_yaml")
+ :- provider(node(DependencyID, Dependency), node(VirtualID, Virtual)),
depends_on(node(ID, Package), node(DependencyID, Dependency)),
facts(Package, provider_preference(Virtual, Dependency, Weight)).
% A provider mentioned in the default configuration can use a weight
% according to its priority in the list of providers
-possible_provider_weight(node(DependencyID, Dependency), node(0, Virtual), Weight, "default")
- :- provider(node(DependencyID, Dependency), node(0, Virtual)),
+possible_provider_weight(node(DependencyID, Dependency), node(VirtualID, Virtual), Weight, "default")
+ :- provider(node(DependencyID, Dependency), node(VirtualID, Virtual)),
default_provider_preference(Virtual, Dependency, Weight).
% Any provider can use 100 as a weight, which is very high and discourage its use
@@ -718,14 +816,14 @@ external_with_variant_set(node(NodeID, Package), Variant, Value)
variant_default_value(Package, Variant, Value)
:- facts(Package, variant_default_value_from_package_py(Variant, Value)),
not variant_default_value_from_packages_yaml(Package, Variant, _),
- not attr("variant_default_value_from_cli", node(0, Package), Variant, _).
+ not attr("variant_default_value_from_cli", node(root_node_id, Package), Variant, _).
variant_default_value(Package, Variant, Value)
:- variant_default_value_from_packages_yaml(Package, Variant, Value),
- not attr("variant_default_value_from_cli", node(0, Package), Variant, _).
+ not attr("variant_default_value_from_cli", node(root_node_id, Package), Variant, _).
variant_default_value(Package, Variant, Value) :-
- attr("variant_default_value_from_cli", node(0, Package), Variant, Value).
+ attr("variant_default_value_from_cli", node(root_node_id, Package), Variant, Value).
% Treat 'none' in a special way - it cannot be combined with other
% values even if the variant is multi-valued
@@ -883,12 +981,12 @@ node_target_weight(node(ID, Package), Weight)
% compatibility rules for targets among nodes
node_target_match(ParentNode, DependencyNode)
- :- depends_on(ParentNode, DependencyNode),
+ :- attr("depends_on", ParentNode, DependencyNode, Type), Type != "build",
attr("node_target", ParentNode, Target),
attr("node_target", DependencyNode, Target).
node_target_mismatch(ParentNode, DependencyNode)
- :- depends_on(ParentNode, DependencyNode),
+ :- attr("depends_on", ParentNode, DependencyNode, Type), Type != "build",
not node_target_match(ParentNode, DependencyNode).
% disallow reusing concrete specs that don't have a compatible target
@@ -1186,9 +1284,14 @@ build_priority(PackageNode, 0) :- attr("node", PackageNode), not optimize_for_
% is displayed (clingo doesn't display sums over empty sets by default)
% Try hard to reuse installed packages (i.e., minimize the number built)
-opt_criterion(100, "number of packages to build (vs. reuse)").
+opt_criterion(110, "number of packages to build (vs. reuse)").
+#minimize { 0@110: #true }.
+#minimize { 1@110,PackageNode : build(PackageNode), optimize_for_reuse() }.
+
+opt_criterion(100, "number of nodes from the same package").
#minimize { 0@100: #true }.
-#minimize { 1@100,PackageNode : build(PackageNode), optimize_for_reuse() }.
+#minimize { ID@100,Package : attr("node", node(ID, Package)) }.
+#minimize { ID@100,Package : attr("virtual_node", node(ID, Package)) }.
#defined optimize_for_reuse/0.
% A condition group specifies one or more specs that must be satisfied.
@@ -1369,34 +1472,6 @@ opt_criterion(5, "non-preferred targets").
build_priority(PackageNode, Priority)
}.
-%-----------------
-% Domain heuristic
-%-----------------
-#heuristic literal_solved(ID) : literal(ID). [1, sign]
-#heuristic literal_solved(ID) : literal(ID). [50, init]
-#heuristic attr("hash", PackageNode, Hash) : attr("root", PackageNode). [45, init]
-
-#heuristic attr("version", node(0, Package), Version) : facts(Package, version_declared(Version, 0)), attr("root", node(0, Package)). [40, true]
-#heuristic version_weight(node(0, Package), 0) : facts(Package, version_declared(Version, 0)), attr("root", node(0, Package)). [40, true]
-#heuristic attr("variant_value", node(0, Package), Variant, Value) : variant_default_value(Package, Variant, Value), attr("root", node(0, Package)). [40, true]
-#heuristic attr("node_target", node(0, Package), Target) : facts(Package, target_weight(Target, 0)), attr("root", node(0, Package)). [40, true]
-#heuristic node_target_weight(node(0, Package), 0) : attr("root", node(0, Package)). [40, true]
-#heuristic node_compiler(node(0, Package), CompilerID) : default_compiler_preference(ID, 0), compiler_id(ID), attr("root", node(0, Package)). [40, true]
-
-#heuristic provider(PackageNode, VirtualNode) : possible_provider_weight(PackageNode, VirtualNode, 0, _), attr("virtual_node", VirtualNode). [30, true]
-#heuristic provider_weight(Package, Virtual, 0, R) : possible_provider_weight(Package, Virtual, 0, R), attr("virtual_node", Virtual). [30, true]
-#heuristic attr("node", Package) : possible_provider_weight(Package, Virtual, 0, _), attr("virtual_node", Virtual). [30, true]
-
-#heuristic attr("version", node(ID, Package), Version) : facts(Package, version_declared(Version, 0)), attr("node", node(ID, Package)). [20, true]
-#heuristic version_weight(node(ID, Package), 0) : facts(Package, version_declared(Version, 0)), attr("node", node(ID, Package)). [20, true]
-
-#heuristic attr("node_target", node(ID, Package), Target) : facts(Package, target_weight(Target, 0)), attr("node", node(ID, Package)). [20, true]
-#heuristic node_target_weight(Package, 0) : attr("node", Package). [20, true]
-#heuristic node_compiler(node(ID, Package), ID) : default_compiler_preference(ID, 0), compiler_id(ID), attr("node", node(NodeID, Package)). [15, true]
-
-#heuristic attr("variant_value", node(ID, Package), Variant, Value) : variant_default_value(Package, Variant, Value), attr("node", node(ID, Package)). [10, true]
-#heuristic attr("node_os", PackageNode, OS) : buildable_os(OS). [10, true]
-
%-----------
% Notes
%-----------
diff --git a/lib/spack/spack/solver/heuristic.lp b/lib/spack/spack/solver/heuristic.lp
new file mode 100644
index 0000000000..7d923219aa
--- /dev/null
+++ b/lib/spack/spack/solver/heuristic.lp
@@ -0,0 +1,28 @@
+% Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
+% Spack Project Developers. See the top-level COPYRIGHT file for details.
+%
+% SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+%-----------------
+% Domain heuristic
+%-----------------
+#heuristic literal_solved(ID) : literal(ID). [1, sign]
+#heuristic literal_solved(ID) : literal(ID). [50, init]
+
+#heuristic attr("hash", node(0, Package), Hash) : literal(_, "root", Package). [45, init]
+#heuristic attr("root", node(0, Package)) : literal(_, "root", Package). [45, true]
+#heuristic attr("node", node(0, Package)) : literal(_, "root", Package). [45, true]
+#heuristic attr("node", node(0, Package)) : literal(_, "node", Package). [45, true]
+
+#heuristic attr("version", node(0, Package), Version) : facts(Package, version_declared(Version, 0)), attr("root", node(0, Package)). [40, true]
+#heuristic version_weight(node(0, Package), 0) : facts(Package, version_declared(Version, 0)), attr("root", node(0, Package)). [40, true]
+#heuristic attr("variant_value", node(0, Package), Variant, Value) : variant_default_value(Package, Variant, Value), attr("root", node(0, Package)). [40, true]
+#heuristic attr("node_target", node(0, Package), Target) : facts(Package, target_weight(Target, 0)), attr("root", node(0, Package)). [40, true]
+#heuristic node_target_weight(node(0, Package), 0) : attr("root", node(0, Package)). [40, true]
+#heuristic node_compiler(node(0, Package), CompilerID) : default_compiler_preference(ID, 0), compiler_id(ID), attr("root", node(0, Package)). [40, true]
+
+#heuristic version_weight(node(0, Package), 0) : attr("node", node(0, Package)). [20, true]
+
+#heuristic attr("node_target", node(0, Package), Target) : facts(Package, target_weight(Target, 0)), attr("node", node(0, Package)). [20, true]
+#heuristic node_target_weight(PackageNode, 0) : attr("node", PackageNode). [20, true]
+#heuristic node_compiler(node(0, Package), ID) : default_compiler_preference(ID, 0), compiler_id(ID), attr("node", node(0, Package)). [15, true]