Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce types.orderOf #97392

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ checkConfigError 'infinite recursion encountered' config.foo ./freeform-attrsOf.
checkConfigError 'The option .* is used but not defined' config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix
checkConfigOutput 24 config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix ./define-value-string.nix

## orderOf
# Check whether two elements with after/before are ordered correctly
checkConfigOutput e config.value.0.data ./order/single-option.nix ./order/orderable.nix
checkConfigOutput d config.value.1.data ./order/single-option.nix ./order/orderable.nix
checkConfigOutput c config.value.2.data ./order/single-option.nix ./order/orderable.nix
checkConfigOutput b config.value.3.data ./order/single-option.nix ./order/orderable.nix
checkConfigOutput a config.value.4.data ./order/single-option.nix ./order/orderable.nix
# Check that a cycle throws an error
checkConfigError 'Cycle detected when trying to order option .*: a -> e -> d -> b -> a' config.value ./order/single-option.nix ./order/cycle.nix
# Check that multiple orderOf declarations can be merged
checkConfigOutput e config.value.0.data ./order/multiple-options.nix ./order/orderable.nix
checkConfigOutput d config.value.1.data ./order/multiple-options.nix ./order/orderable.nix
checkConfigOutput c config.value.2.data ./order/multiple-options.nix ./order/orderable.nix
checkConfigOutput b config.value.3.data ./order/multiple-options.nix ./order/orderable.nix
checkConfigOutput a config.value.4.data ./order/multiple-options.nix ./order/orderable.nix

cat <<EOF
====== module tests ======
$pass Pass
Expand Down
5 changes: 5 additions & 0 deletions lib/tests/modules/order/cycle.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
imports = [ ./orderable.nix ];

value.a.before.e = true;
}
42 changes: 42 additions & 0 deletions lib/tests/modules/order/multiple-options.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{ lib, ... }: {
imports = [
{
options.value = lib.mkOption {
type = lib.types.orderOf {
elemType = lib.types.submodule ({ name, ... }: {
options.data = lib.mkOption {
type = lib.types.str;
default = name;
};
});
};
};
}
{
options.value = lib.mkOption {
type = lib.types.orderOf {
before = a: b: a.value.before.${b.name} or false;
elemType = lib.types.submodule {
options.before = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = {};
};
};
};
};
}
{
options.value = lib.mkOption {
type = lib.types.orderOf {
before = a: b: b.value.after.${a.name} or false;
elemType = lib.types.submodule {
options.after = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = {};
};
};
};
};
}
];
}
23 changes: 23 additions & 0 deletions lib/tests/modules/order/orderable.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{

config.value = {
e = {
before.c = true;
before.a = true;
};
d = {
after.e = true;
};
c = {
after.e = true;
before.b = true;
};
b = {
after.d = true;
before.a = true;
};
a = {
after.d = true;
};
};
}
22 changes: 22 additions & 0 deletions lib/tests/modules/order/single-option.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{ lib, ... }:
{
options.value = lib.mkOption {
type = lib.types.orderOf {
before = a: b: a.value.before.${b.name} or false || b.value.after.${a.name} or false;
elemType = lib.types.submodule ({ name, ... }: {
options.data = lib.mkOption {
type = lib.types.str;
default = name;
};
options.after = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = {};
};
options.before = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = {};
};
});
};
};
}
29 changes: 29 additions & 0 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,35 @@ rec {
functor = (defaultFunctor name) // { wrapped = elemType; };
};


orderOf = {
before ? a: b: false,
elemType
}: mkOptionType rec {
name = "orderOf";
description = "ordered entries of ${elemType.description}s";
check = elemType.check;
merge = loc: defs:
let
nodeAttributes = (attrsOf elemType).merge loc defs;
entries = mapAttrsToList nameValuePair nodeAttributes;
sortedEntries = toposort before entries;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toposort is O(n^2), so I’m kinda afraid this might slow down the module system even more if it’s used in a few places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's possible to implement toposort more efficiently. However I can look into whether it's possible to implement a different interface more efficiently, e.g. something that isn't based on a before function but on a successor mapping instead, e.g. graphToposort { foo = [ "bar" ]; bar = []; } representing foo having a directed edge to bar. In theory, topo sorts can be O(|N| + |E|), though that might not be implementable in Nix.

cycleError =
let showCycle = cycle: concatMapStringsSep " -> " (x: x.name) (reverseList (cycle ++ [ (head cycle) ]));
in throw "Cycle detected when trying to order option `${showOption loc}': ${showCycle sortedEntries.cycle}";
in if sortedEntries ? result then map (v: v.value) sortedEntries.result else cycleError;
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: orderOf { inherit before; elemType = elemType.substSubModules m; };
functor = defaultFunctor name // {
payload = { inherit before elemType; };
binOp = lhs: rhs: {
before = a: b: lhs.before a b || rhs.before a b;
elemType = lhs.elemType.typeMerge rhs.elemType.functor;
};
};
};

# A version of attrsOf that's lazy in its values at the expense of
# conditional definitions not working properly. E.g. defining a value with
# `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
Expand Down
100 changes: 100 additions & 0 deletions nixos/doc/manual/development/option-types.xml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,106 @@
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>
<varname>types.orderOf</varname> {
<replaceable>before</replaceable> ? a: b: false,
<replaceable>elemType</replaceable> }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<replaceable>elemType</replaceable> }
<replaceable>wrappedType</replaceable> }

elemType would have been the entire attrsOf foo for example, despite the name hinting that it'd only be elemType = foo.

</term>
<listitem>
<para>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that types can produce a result that is of a very different shape is non-obvious but essential to the understanding of orderOf, so it seems like a good idea to briefly explain this aspect of orderOf first.

A set of elements of type <replaceable>elemType</replaceable>, ordered
according to the given partial ordering function <replaceable>before
</replaceable>, equivalent to a
<link xlink:href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">
DAG (directed acyclic graph)</link>. This type takes an attribute set
with the following attributes as a parameter:
<itemizedlist>
<listitem><para>
<replaceable>elemType</replaceable>
The type of elements. The <literal>orderOf</literal> type accepts
the same values as <literal>attrsOf elemType</literal> accepts.
</para></listitem>
<listitem><para> <replaceable>before</replaceable>
The partial ordering function. It takes two parameters, each an
attribute set of the form
<variablelist>
<varlistentry>
<term><varname>name</varname></term>
<listitem>
<para>
The attribute name of the entry.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>value</varname></term>
<listitem>
<para>
The attribute value of the entry. This is of type <replaceable>elemType</replaceable>.
</para>
</listitem>
</varlistentry>
</variablelist>
This function should return <literal>true</literal> if the first parameter should be ordered <emphasis>before</emphasis> the second one.
</para></listitem>
</itemizedlist>
The result of this type is a list of <replaceable>elemType</replaceable>
items, ordered according to the <replaceable>before</replaceable> function.
If there are multiple possible orderings an arbitrary one of them is chosen.
</para>
<example xml:id="ex-orderOf"><title><literal>orderOf</literal> Example</title><para>
Here is a simple example of using this type to order a list of text
entries by allowing them to specify which entries they should come
before of.
<programlisting>
{ lib, ... }:
{
options.value = lib.mkOption {
type = lib.types.orderOf {
# a should come before b if the value of a specifies { before.&lt;b&gt; = true; }
before = a: b: a.value.before.${b.name} or false;
elemType = lib.types.submodule {
options.text = lib.mkOption {
type = lib.types.str;
};
options.before = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = {};
};
};
};
};

config.value = {
foo = {
text = "This is foo";
before.bar = true;
};
bar = {
text = "This is bar";
};
};
}
</programlisting>
This option will evaluate to
<programlisting>
[
{
text = "This is foo";
before = {
bar = true;
};
}
{
text = "This is bar";
before = { };
}
]
</programlisting>
</para></example>
</listitem>
</varlistentry>
<varlistentry>
<term>
<varname>types.lazyAttrsOf</varname> <replaceable>t</replaceable>
Expand Down