lib.types.addCheck (lib.types.submodule {}) doesn't seem to work

I have an option defined like this:

{
  options.range = mkOption {
    type = types.addCheck (types.submodule {
      options = {
        start = mkOption {
          type = types.ints.between 1 100;
          default = 100;
        };
        end = mkOption {
          type = types.ints.between 1 1000;
          default = 200;
        };
      };
    }) (x: (x.start <= x.end);
    default = { };
  };
}

Using it this way doesn’t trigger any error:

{config, ...}: {
  config = {
    range = { start = 10; end = 1; };
    service.foo.extraConfig = let inherit (config.range) start end; in ''
      ${toString start} ${toString end}
    '';
  };

where service.foo.extraConfig is definitely evaluated.

I wonder if my usage of submodule is wrong or any in this case?

Forgot to mention:

  1. There are a few such usages in nixpkgs. Not sure if there is any real difference between them and my example.
  2. Is addCheck the correct way to ensure a submodule’s integrity?

Hey i saw your message on the issue.

So here is a quick solution (and an explanation)

First: addCheck doesnt work for submodules at all. This is not widely known in nixpkgs.
All places that you found there are most likely not working and the maintainers of that code are most likely not aware (yet). Hopefully they will get aware, or we manage to fix addCheck, such that they don’t have to bother at all.

I am not sure if this kind of checking would even work in a lazy way for submodules, since we perform checking before merging. Meaning use cases where range.start is in one module and range.end in another couldn’t be merged because we need to check first. Check is only very shallowly checking the surface.

Solutions:

Method 1:

To ‘check’ a submodule for integrity using the type’s check is not working and may break laziness. To circumvent that problem you can add a message to assertions or warnings (in nixos).

Like this:

({config, ...}: {
  assertions = [
    {
        assertion = config.range.start <= config.range.end;  
        message = "please specify a valid range";
    }
  ];
})

In nixos assertions and warnings are evaluated as part of certain output attributes such as system.build.toplevel

nix-repl> nixos.config.system.build.toplevel
# ... elided
       Failed assertions:
       - please specify a valid range

If not on nixos you could build your own seq or deepSeq and mimic the strategy of nixos in your own module system.

Method 2:

Create a custom type:

A bit more complex, see examples how to create types in nixpkgs :slight_smile:
Benefit: Works with all modules, not just nixos.


Some more insight:

The reason the submodule type check is discarded is this little function in the modules.nix file:
Needs to be fixed, thought this requires some creative idea that i didnt manage to find yet or a complete refactor of the types. :slight_smile:

  fixupOptionType =
    loc: opt:
    if opt.type.getSubModules or null == null then
      opt // { type = opt.type or types.unspecified; }
    else
      lib.trace "DISCARD HAPPENED :)" (
      opt
      // {
        type = opt.type.substSubModules opt.options; # <- This line here re-constructs the submodule type with its native check. That means any added check is always discarded siltently :/
        options = [ ];
      });
nix-repl> eval.options.range
trace: DISCARD HAPPENED :)
{
  __toString = «lambda __toString @ lib/modules.nix:1095:20»;
  _type = "option";
  declarationPositions = [ ... ];
  declarations = [ ... ];
  default = { ... };
  definitions = [ ... ];
  definitionsWithLocations = [ ... ];
  files = [ ... ];
  highestPrio = 1500;
  isDefined = true;
  loc = [ ... ];
  options = [ ... ];
  type = { ... };
  value = { ... };
}
4 Likes

Thanks for the detailed explanation.

Re method 1, if I’m not wrong, this requires every user to check the integrity of range at the call site?

assertions and warnings are so useful that I wonder if it makes sense to make them first-class citizen and be part of evalModules? Then each submodule can simply define these two at types.submodule and do integrity check.

2 Likes