opt-in dynamicism can produce inconsistent system states depending on eval method #1

Open
opened 2026-02-03 15:53:13 +00:00 by pennae · 0 comments

consider this module tree:

{ config, ... }: {
  config.services.foo.settings.threads = config.services.bar.settings.threads;
}

specified dynamicism in this case cannot detect that foo will need to have changes applied to it if bar.settings.threads is changed unless this has been explicitly written down. making value transfers of this sort safe requires that dynamic values are not used outside of the modules that declared them, otherwise a dynamic change to bar could produce a different system config than a monolithic evaluation of the same base config and setting override would produce.

we have a similar problem with types and option reuses:

{ config, options, lib, ... }: {
  options.services.something.context = options.services.different.context;
}

in this case changes to different.context would also entail changes to something.context and vice versa. it is now no longer possible to apply overrides to one of these without also affecting the other, and any type-based checking approach (like inserting tagged bottom types for values during a pre-flight check phase) will not be able to find this dependency. this pattern has been used in nixpkgs in the past to share lists between services.

dynamicism may be unsolvable in the nixos module system in the general case. consider the following module:

{ config, lib, ... }: {
  # imagine integer options foo.a, foo.b, foo.c with defaults 0

  config.foo.a = lib.mkMerge [
    (lib.mkIf (config.foo.b > 9000) (lib.mkForce config.foo.c))
    config.bar.settings threads
  ];

  config.systemd.services.foo.environment.a = toString config.foo.a;
}

in this case a dynamic change to foo.c may propagate to foo.a depending on the rest of the configuration, but it may not. in particular a later change to foo.b may change where foo.a propagates from, and bottom injection will not easily solve this becase the uses of the foo values are all located in the foo module.

this is complicated further by the fact that nixos module types are themselves mergeable and depending on eval order such type merges may not be commutative.

consider this module tree: ``` { config, ... }: { config.services.foo.settings.threads = config.services.bar.settings.threads; } ``` specified dynamicism in this case cannot detect that `foo` will need to have changes applied to it if `bar.settings.threads` is changed unless this has been explicitly written down. making value transfers of this sort safe requires that dynamic values are not used outside of the modules that declared them, otherwise a dynamic change to `bar` could produce a different system config than a monolithic evaluation of the same base config and setting override would produce. we have a similar problem with types and option reuses: ``` { config, options, lib, ... }: { options.services.something.context = options.services.different.context; } ``` in this case changes to `different.context` would also entail changes to `something.context` and vice versa. it is now no longer possible to apply overrides to *one* of these without also affecting the *other*, and any type-based checking approach (like inserting tagged bottom types for values during a pre-flight check phase) will not be able to find this dependency. this pattern has been used in nixpkgs in the past to share lists between services. dynamicism may be unsolvable in the nixos module system in the general case. consider the following module: ``` { config, lib, ... }: { # imagine integer options foo.a, foo.b, foo.c with defaults 0 config.foo.a = lib.mkMerge [ (lib.mkIf (config.foo.b > 9000) (lib.mkForce config.foo.c)) config.bar.settings threads ]; config.systemd.services.foo.environment.a = toString config.foo.a; } ``` in this case a dynamic change to `foo.c` may propagate to `foo.a` depending on the rest of the configuration, but it may not. in particular a later change to `foo.b` may change where `foo.a` propagates *from*, and bottom injection will not easily solve this becase the uses of the `foo` values are all located in the `foo` module. this is complicated further by the fact that nixos module types are themselves mergeable and *depending on eval order* such type merges may not be commutative.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
qyriad/rfd-modular-dynamicism#1
No description provided.