Merge pull request #311434 from lucasew/20240513-xrdp-and-friends
[NixPkgs.git] / lib / types.nix
blob518b987dcd92bf83e5094225bbe59c6eb4bf6859
1 # Definitions related to run-time type checking.  Used in particular
2 # to type-check NixOS configurations.
3 { lib }:
5 let
6   inherit (lib)
7     elem
8     flip
9     isAttrs
10     isBool
11     isDerivation
12     isFloat
13     isFunction
14     isInt
15     isList
16     isString
17     isStorePath
18     throwIf
19     toDerivation
20     toList
21     ;
22   inherit (lib.lists)
23     all
24     concatLists
25     count
26     elemAt
27     filter
28     foldl'
29     head
30     imap1
31     last
32     length
33     tail
34     ;
35   inherit (lib.attrsets)
36     attrNames
37     filterAttrs
38     hasAttr
39     mapAttrs
40     optionalAttrs
41     zipAttrsWith
42     ;
43   inherit (lib.options)
44     getFiles
45     getValues
46     mergeDefaultOption
47     mergeEqualOption
48     mergeOneOption
49     mergeUniqueOption
50     showFiles
51     showOption
52     ;
53   inherit (lib.strings)
54     concatMapStringsSep
55     concatStringsSep
56     escapeNixString
57     hasInfix
58     isStringLike
59     ;
60   inherit (lib.trivial)
61     boolToString
62     ;
64   inherit (lib.modules)
65     mergeDefinitions
66     fixupOptionType
67     mergeOptionDecls
68     ;
70   inAttrPosSuffix = v: name:
71     let pos = builtins.unsafeGetAttrPos name v; in
72     if pos == null then "" else " at ${pos.file}:${toString pos.line}:${toString pos.column}";
74   outer_types =
75 rec {
76   __attrsFailEvaluation = true;
77   isType = type: x: (x._type or "") == type;
79   setType = typeName: value: value // {
80     _type = typeName;
81   };
84   # Default type merging function
85   # takes two type functors and return the merged type
86   defaultTypeMerge = f: f':
87     let wrapped = f.wrapped.typeMerge f'.wrapped.functor;
88         payload = f.binOp f.payload f'.payload;
89     in
90     # cannot merge different types
91     if f.name != f'.name
92        then null
93     # simple types
94     else if    (f.wrapped == null && f'.wrapped == null)
95             && (f.payload == null && f'.payload == null)
96        then f.type
97     # composed types
98     else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
99        then f.type wrapped
100     # value types
101     else if (f.payload != null && f'.payload != null) && (payload != null)
102        then f.type payload
103     else null;
105   # Default type functor
106   defaultFunctor = name: {
107     inherit name;
108     type    = types.${name} or null;
109     wrapped = null;
110     payload = null;
111     binOp   = a: b: null;
112   };
114   isOptionType = isType "option-type";
115   mkOptionType =
116     { # Human-readable representation of the type, should be equivalent to
117       # the type function name.
118       name
119     , # Description of the type, defined recursively by embedding the wrapped type if any.
120       description ? null
121       # A hint for whether or not this description needs parentheses. Possible values:
122       #  - "noun": a noun phrase
123       #    Example description: "positive integer",
124       #  - "conjunction": a phrase with a potentially ambiguous "or" connective
125       #    Example description: "int or string"
126       #  - "composite": a phrase with an "of" connective
127       #    Example description: "list of string"
128       #  - "nonRestrictiveClause": a noun followed by a comma and a clause
129       #    Example description: "positive integer, meaning >0"
130       # See the `optionDescriptionPhrase` function.
131     , descriptionClass ? null
132     , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
133       # Function applied to each definition that must return false when a definition
134       # does not match the type. It should not check more than the root of the value,
135       # because checking nested values reduces laziness, leading to unnecessary
136       # infinite recursions in the module system.
137       # Further checks of nested values should be performed by throwing in
138       # the merge function.
139       # Strict and deep type checking can be performed by calling lib.deepSeq on
140       # the merged value.
141       #
142       # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change,
143       # https://github.com/NixOS/nixpkgs/pull/173568 and
144       # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this,
145       # https://github.com/NixOS/nixpkgs/issues/191124 and
146       # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore
147       # this disclaimer.
148       check ? (x: true)
149     , # Merge a list of definitions together into a single value.
150       # This function is called with two arguments: the location of
151       # the option in the configuration as a list of strings
152       # (e.g. ["boot" "loader "grub" "enable"]), and a list of
153       # definition values and locations (e.g. [ { file = "/foo.nix";
154       # value = 1; } { file = "/bar.nix"; value = 2 } ]).
155       merge ? mergeDefaultOption
156     , # Whether this type has a value representing nothingness. If it does,
157       # this should be a value of the form { value = <the nothing value>; }
158       # If it doesn't, this should be {}
159       # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
160       emptyValue ? {}
161     , # Return a flat attrset of sub-options.  Used to generate
162       # documentation.
163       getSubOptions ? prefix: {}
164     , # List of modules if any, or null if none.
165       getSubModules ? null
166     , # Function for building the same option type with a different list of
167       # modules.
168       substSubModules ? m: null
169     , # Function that merge type declarations.
170       # internal, takes a functor as argument and returns the merged type.
171       # returning null means the type is not mergeable
172       typeMerge ? defaultTypeMerge functor
173     , # The type functor.
174       # internal, representation of the type as an attribute set.
175       #   name: name of the type
176       #   type: type function.
177       #   wrapped: the type wrapped in case of compound types.
178       #   payload: values of the type, two payloads of the same type must be
179       #            combinable with the binOp binary operation.
180       #   binOp: binary operation that merge two payloads of the same type.
181       functor ? defaultFunctor name
182     , # The deprecation message to display when this type is used by an option
183       # If null, the type isn't deprecated
184       deprecationMessage ? null
185     , # The types that occur in the definition of this type. This is used to
186       # issue deprecation warnings recursively. Can also be used to reuse
187       # nested types
188       nestedTypes ? {}
189     }:
190     { _type = "option-type";
191       inherit
192         name check merge emptyValue getSubOptions getSubModules substSubModules
193         typeMerge functor deprecationMessage nestedTypes descriptionClass;
194       description = if description == null then name else description;
195     };
197   # optionDescriptionPhrase :: (str -> bool) -> optionType -> str
198   #
199   # Helper function for producing unambiguous but readable natural language
200   # descriptions of types.
201   #
202   # Parameters
203   #
204   #     optionDescriptionPhase unparenthesize optionType
205   #
206   # `unparenthesize`: A function from descriptionClass string to boolean.
207   #   It must return true when the class of phrase will fit unambiguously into
208   #   the description of the caller.
209   #
210   # `optionType`: The option type to parenthesize or not.
211   #   The option whose description we're returning.
212   #
213   # Return value
214   #
215   # The description of the `optionType`, with parentheses if there may be an
216   # ambiguity.
217   optionDescriptionPhrase = unparenthesize: t:
218     if unparenthesize (t.descriptionClass or null)
219     then t.description
220     else "(${t.description})";
222   # When adding new types don't forget to document them in
223   # nixos/doc/manual/development/option-types.xml!
224   types = rec {
226     raw = mkOptionType {
227       name = "raw";
228       description = "raw value";
229       descriptionClass = "noun";
230       check = value: true;
231       merge = mergeOneOption;
232     };
234     anything = mkOptionType {
235       name = "anything";
236       description = "anything";
237       descriptionClass = "noun";
238       check = value: true;
239       merge = loc: defs:
240         let
241           getType = value:
242             if isAttrs value && isStringLike value
243             then "stringCoercibleSet"
244             else builtins.typeOf value;
246           # Returns the common type of all definitions, throws an error if they
247           # don't have the same type
248           commonType = foldl' (type: def:
249             if getType def.value == type
250             then type
251             else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
252           ) (getType (head defs).value) defs;
254           mergeFunction = {
255             # Recursively merge attribute sets
256             set = (attrsOf anything).merge;
257             # Safe and deterministic behavior for lists is to only accept one definition
258             # listOf only used to apply mkIf and co.
259             list =
260               if length defs > 1
261               then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}."
262               else (listOf anything).merge;
263             # This is the type of packages, only accept a single definition
264             stringCoercibleSet = mergeOneOption;
265             lambda = loc: defs: arg: anything.merge
266               (loc ++ [ "<function body>" ])
267               (map (def: {
268                 file = def.file;
269                 value = def.value arg;
270               }) defs);
271             # Otherwise fall back to only allowing all equal definitions
272           }.${commonType} or mergeEqualOption;
273         in mergeFunction loc defs;
274     };
276     unspecified = mkOptionType {
277       name = "unspecified";
278       description = "unspecified value";
279       descriptionClass = "noun";
280     };
282     bool = mkOptionType {
283       name = "bool";
284       description = "boolean";
285       descriptionClass = "noun";
286       check = isBool;
287       merge = mergeEqualOption;
288     };
290     boolByOr = mkOptionType {
291       name = "boolByOr";
292       description = "boolean (merged using or)";
293       descriptionClass = "noun";
294       check = isBool;
295       merge = loc: defs:
296         foldl'
297           (result: def:
298             # Under the assumption that .check always runs before merge, we can assume that all defs.*.value
299             # have been forced, and therefore we assume we don't introduce order-dependent strictness here
300             result || def.value
301           )
302           false
303           defs;
304     };
306     int = mkOptionType {
307       name = "int";
308       description = "signed integer";
309       descriptionClass = "noun";
310       check = isInt;
311       merge = mergeEqualOption;
312     };
314     # Specialized subdomains of int
315     ints =
316       let
317         betweenDesc = lowest: highest:
318           "${toString lowest} and ${toString highest} (both inclusive)";
319         between = lowest: highest:
320           assert lib.assertMsg (lowest <= highest)
321             "ints.between: lowest must be smaller than highest";
322           addCheck int (x: x >= lowest && x <= highest) // {
323             name = "intBetween";
324             description = "integer between ${betweenDesc lowest highest}";
325           };
326         ign = lowest: highest: name: docStart:
327           between lowest highest // {
328             inherit name;
329             description = docStart + "; between ${betweenDesc lowest highest}";
330           };
331         unsign = bit: range: ign 0 (range - 1)
332           "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
333         sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1)
334           "signedInt${toString bit}" "${toString bit} bit signed integer";
336       in {
337         # TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md
338         /**
339           An int with a fixed range.
341           # Example
342           :::{.example}
343           ## `lib.types.ints.between` usage example
345           ```nix
346           (ints.between 0 100).check (-1)
347           => false
348           (ints.between 0 100).check (101)
349           => false
350           (ints.between 0 0).check 0
351           => true
352           ```
354           :::
355         */
356         inherit between;
358         unsigned = addCheck types.int (x: x >= 0) // {
359           name = "unsignedInt";
360           description = "unsigned integer, meaning >=0";
361           descriptionClass = "nonRestrictiveClause";
362         };
363         positive = addCheck types.int (x: x > 0) // {
364           name = "positiveInt";
365           description = "positive integer, meaning >0";
366           descriptionClass = "nonRestrictiveClause";
367         };
368         u8 = unsign 8 256;
369         u16 = unsign 16 65536;
370         # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
371         # the smallest int Nix accepts is -2^63 (-9223372036854775807)
372         u32 = unsign 32 4294967296;
373         # u64 = unsign 64 18446744073709551616;
375         s8 = sign 8 256;
376         s16 = sign 16 65536;
377         s32 = sign 32 4294967296;
378       };
380     # Alias of u16 for a port number
381     port = ints.u16;
383     float = mkOptionType {
384       name = "float";
385       description = "floating point number";
386       descriptionClass = "noun";
387       check = isFloat;
388       merge = mergeEqualOption;
389     };
391     number = either int float;
393     numbers = let
394       betweenDesc = lowest: highest:
395         "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)";
396     in {
397       between = lowest: highest:
398         assert lib.assertMsg (lowest <= highest)
399           "numbers.between: lowest must be smaller than highest";
400         addCheck number (x: x >= lowest && x <= highest) // {
401           name = "numberBetween";
402           description = "integer or floating point number between ${betweenDesc lowest highest}";
403         };
405       nonnegative = addCheck number (x: x >= 0) // {
406         name = "numberNonnegative";
407         description = "nonnegative integer or floating point number, meaning >=0";
408         descriptionClass = "nonRestrictiveClause";
409       };
410       positive = addCheck number (x: x > 0) // {
411         name = "numberPositive";
412         description = "positive integer or floating point number, meaning >0";
413         descriptionClass = "nonRestrictiveClause";
414       };
415     };
417     str = mkOptionType {
418       name = "str";
419       description = "string";
420       descriptionClass = "noun";
421       check = isString;
422       merge = mergeEqualOption;
423     };
425     nonEmptyStr = mkOptionType {
426       name = "nonEmptyStr";
427       description = "non-empty string";
428       descriptionClass = "noun";
429       check = x: str.check x && builtins.match "[ \t\n]*" x == null;
430       inherit (str) merge;
431     };
433     # Allow a newline character at the end and trim it in the merge function.
434     singleLineStr =
435       let
436         inherit (strMatching "[^\n\r]*\n?") check merge;
437       in
438       mkOptionType {
439         name = "singleLineStr";
440         description = "(optionally newline-terminated) single-line string";
441         descriptionClass = "noun";
442         inherit check;
443         merge = loc: defs:
444           lib.removeSuffix "\n" (merge loc defs);
445       };
447     strMatching = pattern: mkOptionType {
448       name = "strMatching ${escapeNixString pattern}";
449       description = "string matching the pattern ${pattern}";
450       descriptionClass = "noun";
451       check = x: str.check x && builtins.match pattern x != null;
452       inherit (str) merge;
453     };
455     # Merge multiple definitions by concatenating them (with the given
456     # separator between the values).
457     separatedString = sep: mkOptionType rec {
458       name = "separatedString";
459       description = if sep == ""
460         then "Concatenated string" # for types.string.
461         else "strings concatenated with ${builtins.toJSON sep}"
462       ;
463       descriptionClass = "noun";
464       check = isString;
465       merge = loc: defs: concatStringsSep sep (getValues defs);
466       functor = (defaultFunctor name) // {
467         payload = sep;
468         binOp = sepLhs: sepRhs:
469           if sepLhs == sepRhs then sepLhs
470           else null;
471       };
472     };
474     lines = separatedString "\n";
475     commas = separatedString ",";
476     envVar = separatedString ":";
478     # Deprecated; should not be used because it quietly concatenates
479     # strings, which is usually not what you want.
480     # We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string`
481     string = lib.warn
482       "The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."
483       (separatedString "" // {
484         name = "string";
485       });
487     passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
488       name = "passwdEntry ${entryType.name}";
489       description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons";
490       descriptionClass = "nonRestrictiveClause";
491     };
493     attrs = mkOptionType {
494       name = "attrs";
495       description = "attribute set";
496       check = isAttrs;
497       merge = loc: foldl' (res: def: res // def.value) {};
498       emptyValue = { value = {}; };
499     };
501     # A package is a top-level store path (/nix/store/hash-name). This includes:
502     # - derivations
503     # - more generally, attribute sets with an `outPath` or `__toString` attribute
504     #   pointing to a store path, e.g. flake inputs
505     # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo)
506     # - hardcoded store path literals (/nix/store/hash-foo) or strings without context
507     #   ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
508     # If you don't need a *top-level* store path, consider using pathInStore instead.
509     package = mkOptionType {
510       name = "package";
511       descriptionClass = "noun";
512       check = x: isDerivation x || isStorePath x;
513       merge = loc: defs:
514         let res = mergeOneOption loc defs;
515         in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res)
516           then toDerivation res
517           else res;
518     };
520     shellPackage = package // {
521       check = x: isDerivation x && hasAttr "shellPath" x;
522     };
524     pkgs = addCheck
525       (unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs // {
526         name = "pkgs";
527         descriptionClass = "noun";
528         description = "Nixpkgs package set";
529       })
530       (x: (x._type or null) == "pkgs");
532     path = mkOptionType {
533       name = "path";
534       descriptionClass = "noun";
535       check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/";
536       merge = mergeEqualOption;
537     };
539     pathInStore = mkOptionType {
540       name = "pathInStore";
541       description = "path in the Nix store";
542       descriptionClass = "noun";
543       check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
544       merge = mergeEqualOption;
545     };
547     listOf = elemType: mkOptionType rec {
548       name = "listOf";
549       description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
550       descriptionClass = "composite";
551       check = isList;
552       merge = loc: defs:
553         map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
554           imap1 (m: def':
555             (mergeDefinitions
556               (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
557               elemType
558               [{ inherit (def) file; value = def'; }]
559             ).optionalValue
560           ) def.value
561         ) defs)));
562       emptyValue = { value = []; };
563       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
564       getSubModules = elemType.getSubModules;
565       substSubModules = m: listOf (elemType.substSubModules m);
566       functor = (defaultFunctor name) // { wrapped = elemType; };
567       nestedTypes.elemType = elemType;
568     };
570     nonEmptyListOf = elemType:
571       let list = addCheck (types.listOf elemType) (l: l != []);
572       in list // {
573         description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
574         emptyValue = { }; # no .value attr, meaning unset
575         substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
576       };
578     attrsOf = elemType: mkOptionType rec {
579       name = "attrsOf";
580       description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
581       descriptionClass = "composite";
582       check = isAttrs;
583       merge = loc: defs:
584         mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
585             (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
586           )
587           # Push down position info.
588           (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs)));
589       emptyValue = { value = {}; };
590       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
591       getSubModules = elemType.getSubModules;
592       substSubModules = m: attrsOf (elemType.substSubModules m);
593       functor = (defaultFunctor name) // { wrapped = elemType; };
594       nestedTypes.elemType = elemType;
595     };
597     # A version of attrsOf that's lazy in its values at the expense of
598     # conditional definitions not working properly. E.g. defining a value with
599     # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
600     # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
601     # error that it's not defined. Use only if conditional definitions don't make sense.
602     lazyAttrsOf = elemType: mkOptionType rec {
603       name = "lazyAttrsOf";
604       description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
605       descriptionClass = "composite";
606       check = isAttrs;
607       merge = loc: defs:
608         zipAttrsWith (name: defs:
609           let merged = mergeDefinitions (loc ++ [name]) elemType defs;
610           # mergedValue will trigger an appropriate error when accessed
611           in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
612         )
613         # Push down position info.
614         (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs);
615       emptyValue = { value = {}; };
616       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
617       getSubModules = elemType.getSubModules;
618       substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
619       functor = (defaultFunctor name) // { wrapped = elemType; };
620       nestedTypes.elemType = elemType;
621     };
623     # TODO: deprecate this in the future:
624     loaOf = elemType: types.attrsOf elemType // {
625       name = "loaOf";
626       deprecationMessage = "Mixing lists with attribute values is no longer"
627         + " possible; please use `types.attrsOf` instead. See"
628         + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
629       nestedTypes.elemType = elemType;
630     };
632     attrTag = tags:
633       let tags_ = tags; in
634       let
635         tags =
636           mapAttrs
637             (n: opt:
638               builtins.addErrorContext "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" (
639                 throwIf (opt._type or null != "option")
640                   "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${
641                     if opt?_type then
642                       if opt._type == "option-type"
643                       then "was a bare type, not wrapped in mkOption."
644                       else "was of type ${lib.strings.escapeNixString opt._type}."
645                     else "was not."}"
646                 opt // {
647                   declarations = opt.declarations or (
648                     let pos = builtins.unsafeGetAttrPos n tags_;
649                     in if pos == null then [] else [ pos.file ]
650                   );
651                   declarationPositions = opt.declarationPositions or (
652                     let pos = builtins.unsafeGetAttrPos n tags_;
653                     in if pos == null then [] else [ pos ]
654                   );
655                 }
656               ))
657             tags_;
658         choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags);
659       in
660       mkOptionType {
661         name = "attrTag";
662         description = "attribute-tagged union";
663         descriptionClass = "noun";
664         getSubOptions = prefix:
665           mapAttrs
666             (tagName: tagOption: {
667               "${lib.showOption prefix}" =
668                 tagOption // {
669                   loc = prefix ++ [ tagName ];
670                 };
671             })
672             tags;
673         check = v: isAttrs v && length (attrNames v) == 1 && tags?${head (attrNames v)};
674         merge = loc: defs:
675           let
676             choice = head (attrNames (head defs).value);
677             checkedValueDefs = map
678               (def:
679                 assert (length (attrNames def.value)) == 1;
680                 if (head (attrNames def.value)) != choice
681                 then throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}."
682                 else { inherit (def) file; value = def.value.${choice}; })
683               defs;
684           in
685             if tags?${choice}
686             then
687               { ${choice} =
688                   (lib.modules.evalOptionValue
689                     (loc ++ [choice])
690                     tags.${choice}
691                     checkedValueDefs
692                   ).value;
693               }
694             else throw "The option `${showOption loc}` is defined as ${lib.strings.escapeNixIdentifier choice}, but ${lib.strings.escapeNixIdentifier choice} is not among the valid choices (${choicesStr}). Value ${choice} was defined in ${showFiles (getFiles defs)}.";
695         nestedTypes = tags;
696         functor = defaultFunctor "attrTag" // {
697           type = { tags, ... }: types.attrTag tags;
698           payload = { inherit tags; };
699           binOp =
700             let
701               # Add metadata in the format that submodules work with
702               wrapOptionDecl =
703                 option: { options = option; _file = "<attrTag {...}>"; pos = null; };
704             in
705             a: b: {
706               tags = a.tags // b.tags //
707                 mapAttrs
708                   (tagName: bOpt:
709                     lib.mergeOptionDecls
710                       # FIXME: loc is not accurate; should include prefix
711                       #        Fortunately, it's only used for error messages, where a "relative" location is kinda ok.
712                       #        It is also returned though, but use of the attribute seems rare?
713                       [tagName]
714                       [ (wrapOptionDecl a.tags.${tagName}) (wrapOptionDecl bOpt) ]
715                     // {
716                       # mergeOptionDecls is not idempotent in these attrs:
717                       declarations = a.tags.${tagName}.declarations ++ bOpt.declarations;
718                       declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions;
719                     }
720                   )
721                   (builtins.intersectAttrs a.tags b.tags);
722           };
723         };
724       };
726     uniq = unique { message = ""; };
728     unique = { message }: type: mkOptionType rec {
729       name = "unique";
730       inherit (type) description descriptionClass check;
731       merge = mergeUniqueOption { inherit message; inherit (type) merge; };
732       emptyValue = type.emptyValue;
733       getSubOptions = type.getSubOptions;
734       getSubModules = type.getSubModules;
735       substSubModules = m: uniq (type.substSubModules m);
736       functor = (defaultFunctor name) // { wrapped = type; };
737       nestedTypes.elemType = type;
738     };
740     # Null or value of ...
741     nullOr = elemType: mkOptionType rec {
742       name = "nullOr";
743       description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}";
744       descriptionClass = "conjunction";
745       check = x: x == null || elemType.check x;
746       merge = loc: defs:
747         let nrNulls = count (def: def.value == null) defs; in
748         if nrNulls == length defs then null
749         else if nrNulls != 0 then
750           throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
751         else elemType.merge loc defs;
752       emptyValue = { value = null; };
753       getSubOptions = elemType.getSubOptions;
754       getSubModules = elemType.getSubModules;
755       substSubModules = m: nullOr (elemType.substSubModules m);
756       functor = (defaultFunctor name) // { wrapped = elemType; };
757       nestedTypes.elemType = elemType;
758     };
760     functionTo = elemType: mkOptionType {
761       name = "functionTo";
762       description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
763       descriptionClass = "composite";
764       check = isFunction;
765       merge = loc: defs:
766         fnArgs: (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
767       getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
768       getSubModules = elemType.getSubModules;
769       substSubModules = m: functionTo (elemType.substSubModules m);
770       functor = (defaultFunctor "functionTo") // { wrapped = elemType; };
771       nestedTypes.elemType = elemType;
772     };
774     # A submodule (like typed attribute set). See NixOS manual.
775     submodule = modules: submoduleWith {
776       shorthandOnlyDefinesConfig = true;
777       modules = toList modules;
778     };
780     # A module to be imported in some other part of the configuration.
781     deferredModule = deferredModuleWith { };
783     # A module to be imported in some other part of the configuration.
784     # `staticModules`' options will be added to the documentation, unlike
785     # options declared via `config`.
786     deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
787       name = "deferredModule";
788       description = "module";
789       descriptionClass = "noun";
790       check = x: isAttrs x || isFunction x || path.check x;
791       merge = loc: defs: {
792         imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
793       };
794       inherit (submoduleWith { modules = staticModules; })
795         getSubOptions
796         getSubModules;
797       substSubModules = m: deferredModuleWith (attrs // {
798         staticModules = m;
799       });
800       functor = defaultFunctor "deferredModuleWith" // {
801         type = types.deferredModuleWith;
802         payload = {
803           inherit staticModules;
804         };
805         binOp = lhs: rhs: {
806           staticModules = lhs.staticModules ++ rhs.staticModules;
807         };
808       };
809     };
811     # The type of a type!
812     optionType = mkOptionType {
813       name = "optionType";
814       description = "optionType";
815       descriptionClass = "noun";
816       check = value: value._type or null == "option-type";
817       merge = loc: defs:
818         if length defs == 1
819         then (head defs).value
820         else let
821           # Prepares the type definitions for mergeOptionDecls, which
822           # annotates submodules types with file locations
823           optionModules = map ({ value, file }:
824             {
825               _file = file;
826               # There's no way to merge types directly from the module system,
827               # but we can cheat a bit by just declaring an option with the type
828               options = lib.mkOption {
829                 type = value;
830               };
831             }
832           ) defs;
833           # Merges all the types into a single one, including submodule merging.
834           # This also propagates file information to all submodules
835           mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules);
836         in mergedOption.type;
837     };
839     submoduleWith =
840       { modules
841       , specialArgs ? {}
842       , shorthandOnlyDefinesConfig ? false
843       , description ? null
844       , class ? null
845       }@attrs:
846       let
847         inherit (lib.modules) evalModules;
849         allModules = defs: map ({ value, file }:
850           if isAttrs value && shorthandOnlyDefinesConfig
851           then { _file = file; config = value; }
852           else { _file = file; imports = [ value ]; }
853         ) defs;
855         base = evalModules {
856           inherit class specialArgs;
857           modules = [{
858             # This is a work-around for the fact that some sub-modules,
859             # such as the one included in an attribute set, expects an "args"
860             # attribute to be given to the sub-module. As the option
861             # evaluation does not have any specific attribute name yet, we
862             # provide a default for the documentation and the freeform type.
863             #
864             # This is necessary as some option declaration might use the
865             # "name" attribute given as argument of the submodule and use it
866             # as the default of option declarations.
867             #
868             # We use lookalike unicode single angle quotation marks because
869             # of the docbook transformation the options receive. In all uses
870             # &gt; and &lt; wouldn't be encoded correctly so the encoded values
871             # would be used, and use of `<` and `>` would break the XML document.
872             # It shouldn't cause an issue since this is cosmetic for the manual.
873             _module.args.name = lib.mkOptionDefault "‹name›";
874           }] ++ modules;
875         };
877         freeformType = base._module.freeformType;
879         name = "submodule";
881       in
882       mkOptionType {
883         inherit name;
884         description =
885           if description != null then description
886           else freeformType.description or name;
887         check = x: isAttrs x || isFunction x || path.check x;
888         merge = loc: defs:
889           (base.extendModules {
890             modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
891             prefix = loc;
892           }).config;
893         emptyValue = { value = {}; };
894         getSubOptions = prefix: (base.extendModules
895           { inherit prefix; }).options // optionalAttrs (freeformType != null) {
896             # Expose the sub options of the freeform type. Note that the option
897             # discovery doesn't care about the attribute name used here, so this
898             # is just to avoid conflicts with potential options from the submodule
899             _freeformOptions = freeformType.getSubOptions prefix;
900           };
901         getSubModules = modules;
902         substSubModules = m: submoduleWith (attrs // {
903           modules = m;
904         });
905         nestedTypes = lib.optionalAttrs (freeformType != null) {
906           freeformType = freeformType;
907         };
908         functor = defaultFunctor name // {
909           type = types.submoduleWith;
910           payload = {
911             inherit modules class specialArgs shorthandOnlyDefinesConfig description;
912           };
913           binOp = lhs: rhs: {
914             class =
915               # `or null` was added for backwards compatibility only. `class` is
916               # always set in the current version of the module system.
917               if lhs.class or null == null then rhs.class or null
918               else if rhs.class or null == null then lhs.class or null
919               else if lhs.class or null == rhs.class then lhs.class or null
920               else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\".";
921             modules = lhs.modules ++ rhs.modules;
922             specialArgs =
923               let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
924               in if intersecting == {}
925               then lhs.specialArgs // rhs.specialArgs
926               else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
927             shorthandOnlyDefinesConfig =
928               if lhs.shorthandOnlyDefinesConfig == null
929               then rhs.shorthandOnlyDefinesConfig
930               else if rhs.shorthandOnlyDefinesConfig == null
931               then lhs.shorthandOnlyDefinesConfig
932               else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
933               then lhs.shorthandOnlyDefinesConfig
934               else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
935             description =
936               if lhs.description == null
937               then rhs.description
938               else if rhs.description == null
939               then lhs.description
940               else if lhs.description == rhs.description
941               then lhs.description
942               else throw "A submoduleWith option is declared multiple times with conflicting descriptions";
943           };
944         };
945       };
947     # A value from a set of allowed ones.
948     enum = values:
949       let
950         inherit (lib.lists) unique;
951         show = v:
952                if builtins.isString v then ''"${v}"''
953           else if builtins.isInt v then builtins.toString v
954           else if builtins.isBool v then boolToString v
955           else ''<${builtins.typeOf v}>'';
956       in
957       mkOptionType rec {
958         name = "enum";
959         description =
960           # Length 0 or 1 enums may occur in a design pattern with type merging
961           # where an "interface" module declares an empty enum and other modules
962           # provide implementations, each extending the enum with their own
963           # identifier.
964           if values == [] then
965             "impossible (empty enum)"
966           else if builtins.length values == 1 then
967             "value ${show (builtins.head values)} (singular enum)"
968           else
969             "one of ${concatMapStringsSep ", " show values}";
970         descriptionClass =
971           if builtins.length values < 2
972           then "noun"
973           else "conjunction";
974         check = flip elem values;
975         merge = mergeEqualOption;
976         functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
977       };
979     # Either value of type `t1` or `t2`.
980     either = t1: t2: mkOptionType rec {
981       name = "either";
982       description =
983         if t1.descriptionClass or null == "nonRestrictiveClause"
984         then
985           # Plain, but add comma
986           "${t1.description}, or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2}"
987         else
988           "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
989       descriptionClass = "conjunction";
990       check = x: t1.check x || t2.check x;
991       merge = loc: defs:
992         let
993           defList = map (d: d.value) defs;
994         in
995           if   all (x: t1.check x) defList
996                then t1.merge loc defs
997           else if all (x: t2.check x) defList
998                then t2.merge loc defs
999           else mergeOneOption loc defs;
1000       typeMerge = f':
1001         let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
1002             mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
1003         in
1004            if (name == f'.name) && (mt1 != null) && (mt2 != null)
1005            then functor.type mt1 mt2
1006            else null;
1007       functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
1008       nestedTypes.left = t1;
1009       nestedTypes.right = t2;
1010     };
1012     # Any of the types in the given list
1013     oneOf = ts:
1014       let
1015         head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
1016         tail' = tail ts;
1017       in foldl' either head' tail';
1019     # Either value of type `coercedType` or `finalType`, the former is
1020     # converted to `finalType` using `coerceFunc`.
1021     coercedTo = coercedType: coerceFunc: finalType:
1022       assert lib.assertMsg (coercedType.getSubModules == null)
1023         "coercedTo: coercedType must not have submodules (it’s a ${
1024           coercedType.description})";
1025       mkOptionType rec {
1026         name = "coercedTo";
1027         description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it";
1028         check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
1029         merge = loc: defs:
1030           let
1031             coerceVal = val:
1032               if coercedType.check val then coerceFunc val
1033               else val;
1034           in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
1035         emptyValue = finalType.emptyValue;
1036         getSubOptions = finalType.getSubOptions;
1037         getSubModules = finalType.getSubModules;
1038         substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
1039         typeMerge = t1: t2: null;
1040         functor = (defaultFunctor name) // { wrapped = finalType; };
1041         nestedTypes.coercedType = coercedType;
1042         nestedTypes.finalType = finalType;
1043       };
1045     # Augment the given type with an additional type check function.
1046     addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
1048   };
1051 in outer_types // outer_types.types