dump_syms: Add consumers into passthru.tests
[NixPkgs.git] / lib / types.nix
blob270ac1748c7961677c2c3c3dfa5d2e8a54f8c3f4
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     toDerivation
19     toList
20     ;
21   inherit (lib.lists)
22     all
23     concatLists
24     count
25     elemAt
26     filter
27     foldl'
28     head
29     imap1
30     last
31     length
32     tail
33     ;
34   inherit (lib.attrsets)
35     attrNames
36     filterAttrs
37     hasAttr
38     mapAttrs
39     optionalAttrs
40     zipAttrsWith
41     ;
42   inherit (lib.options)
43     getFiles
44     getValues
45     mergeDefaultOption
46     mergeEqualOption
47     mergeOneOption
48     mergeUniqueOption
49     showFiles
50     showOption
51     ;
52   inherit (lib.strings)
53     concatMapStringsSep
54     concatStringsSep
55     escapeNixString
56     hasInfix
57     isCoercibleToString
58     ;
59   inherit (lib.trivial)
60     boolToString
61     ;
63   inherit (lib.modules)
64     mergeDefinitions
65     fixupOptionType
66     mergeOptionDecls
67     ;
68   outer_types =
69 rec {
70   isType = type: x: (x._type or "") == type;
72   setType = typeName: value: value // {
73     _type = typeName;
74   };
77   # Default type merging function
78   # takes two type functors and return the merged type
79   defaultTypeMerge = f: f':
80     let wrapped = f.wrapped.typeMerge f'.wrapped.functor;
81         payload = f.binOp f.payload f'.payload;
82     in
83     # cannot merge different types
84     if f.name != f'.name
85        then null
86     # simple types
87     else if    (f.wrapped == null && f'.wrapped == null)
88             && (f.payload == null && f'.payload == null)
89        then f.type
90     # composed types
91     else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
92        then f.type wrapped
93     # value types
94     else if (f.payload != null && f'.payload != null) && (payload != null)
95        then f.type payload
96     else null;
98   # Default type functor
99   defaultFunctor = name: {
100     inherit name;
101     type    = types.${name} or null;
102     wrapped = null;
103     payload = null;
104     binOp   = a: b: null;
105   };
107   isOptionType = isType "option-type";
108   mkOptionType =
109     { # Human-readable representation of the type, should be equivalent to
110       # the type function name.
111       name
112     , # Description of the type, defined recursively by embedding the wrapped type if any.
113       description ? null
114       # A hint for whether or not this description needs parentheses. Possible values:
115       #  - "noun": a simple noun phrase such as "positive integer"
116       #  - "conjunction": a phrase with a potentially ambiguous "or" connective.
117       #  - "composite": a phrase with an "of" connective
118       # See the `optionDescriptionPhrase` function.
119     , descriptionClass ? null
120     , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
121       # Function applied to each definition that must return false when a definition
122       # does not match the type. It should not check more than the root of the value,
123       # because checking nested values reduces laziness, leading to unnecessary
124       # infinite recursions in the module system.
125       # Further checks of nested values should be performed by throwing in
126       # the merge function.
127       # Strict and deep type checking can be performed by calling lib.deepSeq on
128       # the merged value.
129       #
130       # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change,
131       # https://github.com/NixOS/nixpkgs/pull/173568 and
132       # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this,
133       # https://github.com/NixOS/nixpkgs/issues/191124 and
134       # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore
135       # this disclaimer.
136       check ? (x: true)
137     , # Merge a list of definitions together into a single value.
138       # This function is called with two arguments: the location of
139       # the option in the configuration as a list of strings
140       # (e.g. ["boot" "loader "grub" "enable"]), and a list of
141       # definition values and locations (e.g. [ { file = "/foo.nix";
142       # value = 1; } { file = "/bar.nix"; value = 2 } ]).
143       merge ? mergeDefaultOption
144     , # Whether this type has a value representing nothingness. If it does,
145       # this should be a value of the form { value = <the nothing value>; }
146       # If it doesn't, this should be {}
147       # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
148       emptyValue ? {}
149     , # Return a flat list of sub-options.  Used to generate
150       # documentation.
151       getSubOptions ? prefix: {}
152     , # List of modules if any, or null if none.
153       getSubModules ? null
154     , # Function for building the same option type with a different list of
155       # modules.
156       substSubModules ? m: null
157     , # Function that merge type declarations.
158       # internal, takes a functor as argument and returns the merged type.
159       # returning null means the type is not mergeable
160       typeMerge ? defaultTypeMerge functor
161     , # The type functor.
162       # internal, representation of the type as an attribute set.
163       #   name: name of the type
164       #   type: type function.
165       #   wrapped: the type wrapped in case of compound types.
166       #   payload: values of the type, two payloads of the same type must be
167       #            combinable with the binOp binary operation.
168       #   binOp: binary operation that merge two payloads of the same type.
169       functor ? defaultFunctor name
170     , # The deprecation message to display when this type is used by an option
171       # If null, the type isn't deprecated
172       deprecationMessage ? null
173     , # The types that occur in the definition of this type. This is used to
174       # issue deprecation warnings recursively. Can also be used to reuse
175       # nested types
176       nestedTypes ? {}
177     }:
178     { _type = "option-type";
179       inherit
180         name check merge emptyValue getSubOptions getSubModules substSubModules
181         typeMerge functor deprecationMessage nestedTypes descriptionClass;
182       description = if description == null then name else description;
183     };
185   # optionDescriptionPhrase :: (str -> bool) -> optionType -> str
186   #
187   # Helper function for producing unambiguous but readable natural language
188   # descriptions of types.
189   #
190   # Parameters
191   #
192   #     optionDescriptionPhase unparenthesize optionType
193   #
194   # `unparenthesize`: A function from descriptionClass string to boolean.
195   #   It must return true when the class of phrase will fit unambiguously into
196   #   the description of the caller.
197   #
198   # `optionType`: The option type to parenthesize or not.
199   #   The option whose description we're returning.
200   #
201   # Return value
202   #
203   # The description of the `optionType`, with parentheses if there may be an
204   # ambiguity.
205   optionDescriptionPhrase = unparenthesize: t:
206     if unparenthesize (t.descriptionClass or null)
207     then t.description
208     else "(${t.description})";
210   # When adding new types don't forget to document them in
211   # nixos/doc/manual/development/option-types.xml!
212   types = rec {
214     raw = mkOptionType rec {
215       name = "raw";
216       description = "raw value";
217       descriptionClass = "noun";
218       check = value: true;
219       merge = mergeOneOption;
220     };
222     anything = mkOptionType {
223       name = "anything";
224       description = "anything";
225       descriptionClass = "noun";
226       check = value: true;
227       merge = loc: defs:
228         let
229           getType = value:
230             if isAttrs value && isCoercibleToString value
231             then "stringCoercibleSet"
232             else builtins.typeOf value;
234           # Returns the common type of all definitions, throws an error if they
235           # don't have the same type
236           commonType = foldl' (type: def:
237             if getType def.value == type
238             then type
239             else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
240           ) (getType (head defs).value) defs;
242           mergeFunction = {
243             # Recursively merge attribute sets
244             set = (attrsOf anything).merge;
245             # Safe and deterministic behavior for lists is to only accept one definition
246             # listOf only used to apply mkIf and co.
247             list =
248               if length defs > 1
249               then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}."
250               else (listOf anything).merge;
251             # This is the type of packages, only accept a single definition
252             stringCoercibleSet = mergeOneOption;
253             lambda = loc: defs: arg: anything.merge
254               (loc ++ [ "<function body>" ])
255               (map (def: {
256                 file = def.file;
257                 value = def.value arg;
258               }) defs);
259             # Otherwise fall back to only allowing all equal definitions
260           }.${commonType} or mergeEqualOption;
261         in mergeFunction loc defs;
262     };
264     unspecified = mkOptionType {
265       name = "unspecified";
266       description = "unspecified value";
267       descriptionClass = "noun";
268     };
270     bool = mkOptionType {
271       name = "bool";
272       description = "boolean";
273       descriptionClass = "noun";
274       check = isBool;
275       merge = mergeEqualOption;
276     };
278     int = mkOptionType {
279       name = "int";
280       description = "signed integer";
281       descriptionClass = "noun";
282       check = isInt;
283       merge = mergeEqualOption;
284     };
286     # Specialized subdomains of int
287     ints =
288       let
289         betweenDesc = lowest: highest:
290           "${toString lowest} and ${toString highest} (both inclusive)";
291         between = lowest: highest:
292           assert lib.assertMsg (lowest <= highest)
293             "ints.between: lowest must be smaller than highest";
294           addCheck int (x: x >= lowest && x <= highest) // {
295             name = "intBetween";
296             description = "integer between ${betweenDesc lowest highest}";
297           };
298         ign = lowest: highest: name: docStart:
299           between lowest highest // {
300             inherit name;
301             description = docStart + "; between ${betweenDesc lowest highest}";
302           };
303         unsign = bit: range: ign 0 (range - 1)
304           "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
305         sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1)
306           "signedInt${toString bit}" "${toString bit} bit signed integer";
308       in {
309         /* An int with a fixed range.
310         *
311         * Example:
312         *   (ints.between 0 100).check (-1)
313         *   => false
314         *   (ints.between 0 100).check (101)
315         *   => false
316         *   (ints.between 0 0).check 0
317         *   => true
318         */
319         inherit between;
321         unsigned = addCheck types.int (x: x >= 0) // {
322           name = "unsignedInt";
323           description = "unsigned integer, meaning >=0";
324         };
325         positive = addCheck types.int (x: x > 0) // {
326           name = "positiveInt";
327           description = "positive integer, meaning >0";
328         };
329         u8 = unsign 8 256;
330         u16 = unsign 16 65536;
331         # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
332         # the smallest int Nix accepts is -2^63 (-9223372036854775807)
333         u32 = unsign 32 4294967296;
334         # u64 = unsign 64 18446744073709551616;
336         s8 = sign 8 256;
337         s16 = sign 16 65536;
338         s32 = sign 32 4294967296;
339       };
341     # Alias of u16 for a port number
342     port = ints.u16;
344     float = mkOptionType {
345       name = "float";
346       description = "floating point number";
347       descriptionClass = "noun";
348       check = isFloat;
349       merge = mergeEqualOption;
350     };
352     number = either int float;
354     numbers = let
355       betweenDesc = lowest: highest:
356         "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)";
357     in {
358       between = lowest: highest:
359         assert lib.assertMsg (lowest <= highest)
360           "numbers.between: lowest must be smaller than highest";
361         addCheck number (x: x >= lowest && x <= highest) // {
362           name = "numberBetween";
363           description = "integer or floating point number between ${betweenDesc lowest highest}";
364         };
366       nonnegative = addCheck number (x: x >= 0) // {
367         name = "numberNonnegative";
368         description = "nonnegative integer or floating point number, meaning >=0";
369       };
370       positive = addCheck number (x: x > 0) // {
371         name = "numberPositive";
372         description = "positive integer or floating point number, meaning >0";
373       };
374     };
376     str = mkOptionType {
377       name = "str";
378       description = "string";
379       descriptionClass = "noun";
380       check = isString;
381       merge = mergeEqualOption;
382     };
384     nonEmptyStr = mkOptionType {
385       name = "nonEmptyStr";
386       description = "non-empty string";
387       descriptionClass = "noun";
388       check = x: str.check x && builtins.match "[ \t\n]*" x == null;
389       inherit (str) merge;
390     };
392     # Allow a newline character at the end and trim it in the merge function.
393     singleLineStr =
394       let
395         inherit (strMatching "[^\n\r]*\n?") check merge;
396       in
397       mkOptionType {
398         name = "singleLineStr";
399         description = "(optionally newline-terminated) single-line string";
400         descriptionClass = "noun";
401         inherit check;
402         merge = loc: defs:
403           lib.removeSuffix "\n" (merge loc defs);
404       };
406     strMatching = pattern: mkOptionType {
407       name = "strMatching ${escapeNixString pattern}";
408       description = "string matching the pattern ${pattern}";
409       descriptionClass = "noun";
410       check = x: str.check x && builtins.match pattern x != null;
411       inherit (str) merge;
412     };
414     # Merge multiple definitions by concatenating them (with the given
415     # separator between the values).
416     separatedString = sep: mkOptionType rec {
417       name = "separatedString";
418       description = if sep == ""
419         then "Concatenated string" # for types.string.
420         else "strings concatenated with ${builtins.toJSON sep}"
421       ;
422       descriptionClass = "noun";
423       check = isString;
424       merge = loc: defs: concatStringsSep sep (getValues defs);
425       functor = (defaultFunctor name) // {
426         payload = sep;
427         binOp = sepLhs: sepRhs:
428           if sepLhs == sepRhs then sepLhs
429           else null;
430       };
431     };
433     lines = separatedString "\n";
434     commas = separatedString ",";
435     envVar = separatedString ":";
437     # Deprecated; should not be used because it quietly concatenates
438     # strings, which is usually not what you want.
439     string = separatedString "" // {
440       name = "string";
441       deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types.";
442     };
444     passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
445       name = "passwdEntry ${entryType.name}";
446       description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons";
447     };
449     attrs = mkOptionType {
450       name = "attrs";
451       description = "attribute set";
452       check = isAttrs;
453       merge = loc: foldl' (res: def: res // def.value) {};
454       emptyValue = { value = {}; };
455     };
457     # A package is a top-level store path (/nix/store/hash-name). This includes:
458     # - derivations
459     # - more generally, attribute sets with an `outPath` or `__toString` attribute
460     #   pointing to a store path, e.g. flake inputs
461     # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo)
462     # - hardcoded store path literals (/nix/store/hash-foo) or strings without context
463     #   ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
464     package = mkOptionType {
465       name = "package";
466       descriptionClass = "noun";
467       check = x: isDerivation x || isStorePath x;
468       merge = loc: defs:
469         let res = mergeOneOption loc defs;
470         in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res)
471           then toDerivation res
472           else res;
473     };
475     shellPackage = package // {
476       check = x: isDerivation x && hasAttr "shellPath" x;
477     };
479     path = mkOptionType {
480       name = "path";
481       descriptionClass = "noun";
482       check = x: isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
483       merge = mergeEqualOption;
484     };
486     listOf = elemType: mkOptionType rec {
487       name = "listOf";
488       description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
489       descriptionClass = "composite";
490       check = isList;
491       merge = loc: defs:
492         map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
493           imap1 (m: def':
494             (mergeDefinitions
495               (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
496               elemType
497               [{ inherit (def) file; value = def'; }]
498             ).optionalValue
499           ) def.value
500         ) defs)));
501       emptyValue = { value = []; };
502       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
503       getSubModules = elemType.getSubModules;
504       substSubModules = m: listOf (elemType.substSubModules m);
505       functor = (defaultFunctor name) // { wrapped = elemType; };
506       nestedTypes.elemType = elemType;
507     };
509     nonEmptyListOf = elemType:
510       let list = addCheck (types.listOf elemType) (l: l != []);
511       in list // {
512         description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
513         emptyValue = { }; # no .value attr, meaning unset
514       };
516     attrsOf = elemType: mkOptionType rec {
517       name = "attrsOf";
518       description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
519       descriptionClass = "composite";
520       check = isAttrs;
521       merge = loc: defs:
522         mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
523             (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
524           )
525           # Push down position info.
526           (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs)));
527       emptyValue = { value = {}; };
528       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
529       getSubModules = elemType.getSubModules;
530       substSubModules = m: attrsOf (elemType.substSubModules m);
531       functor = (defaultFunctor name) // { wrapped = elemType; };
532       nestedTypes.elemType = elemType;
533     };
535     # A version of attrsOf that's lazy in its values at the expense of
536     # conditional definitions not working properly. E.g. defining a value with
537     # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
538     # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
539     # error that it's not defined. Use only if conditional definitions don't make sense.
540     lazyAttrsOf = elemType: mkOptionType rec {
541       name = "lazyAttrsOf";
542       description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
543       descriptionClass = "composite";
544       check = isAttrs;
545       merge = loc: defs:
546         zipAttrsWith (name: defs:
547           let merged = mergeDefinitions (loc ++ [name]) elemType defs;
548           # mergedValue will trigger an appropriate error when accessed
549           in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
550         )
551         # Push down position info.
552         (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs);
553       emptyValue = { value = {}; };
554       getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
555       getSubModules = elemType.getSubModules;
556       substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
557       functor = (defaultFunctor name) // { wrapped = elemType; };
558       nestedTypes.elemType = elemType;
559     };
561     # TODO: drop this in the future:
562     loaOf = elemType: types.attrsOf elemType // {
563       name = "loaOf";
564       deprecationMessage = "Mixing lists with attribute values is no longer"
565         + " possible; please use `types.attrsOf` instead. See"
566         + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
567       nestedTypes.elemType = elemType;
568     };
570     # Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
571     uniq = elemType: mkOptionType rec {
572       name = "uniq";
573       inherit (elemType) description descriptionClass check;
574       merge = mergeOneOption;
575       emptyValue = elemType.emptyValue;
576       getSubOptions = elemType.getSubOptions;
577       getSubModules = elemType.getSubModules;
578       substSubModules = m: uniq (elemType.substSubModules m);
579       functor = (defaultFunctor name) // { wrapped = elemType; };
580       nestedTypes.elemType = elemType;
581     };
583     unique = { message }: type: mkOptionType rec {
584       name = "unique";
585       inherit (type) description descriptionClass check;
586       merge = mergeUniqueOption { inherit message; };
587       emptyValue = type.emptyValue;
588       getSubOptions = type.getSubOptions;
589       getSubModules = type.getSubModules;
590       substSubModules = m: uniq (type.substSubModules m);
591       functor = (defaultFunctor name) // { wrapped = type; };
592       nestedTypes.elemType = type;
593     };
595     # Null or value of ...
596     nullOr = elemType: mkOptionType rec {
597       name = "nullOr";
598       description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}";
599       descriptionClass = "conjunction";
600       check = x: x == null || elemType.check x;
601       merge = loc: defs:
602         let nrNulls = count (def: def.value == null) defs; in
603         if nrNulls == length defs then null
604         else if nrNulls != 0 then
605           throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
606         else elemType.merge loc defs;
607       emptyValue = { value = null; };
608       getSubOptions = elemType.getSubOptions;
609       getSubModules = elemType.getSubModules;
610       substSubModules = m: nullOr (elemType.substSubModules m);
611       functor = (defaultFunctor name) // { wrapped = elemType; };
612       nestedTypes.elemType = elemType;
613     };
615     functionTo = elemType: mkOptionType {
616       name = "functionTo";
617       description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
618       descriptionClass = "composite";
619       check = isFunction;
620       merge = loc: defs:
621         fnArgs: (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
622       getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
623       getSubModules = elemType.getSubModules;
624       substSubModules = m: functionTo (elemType.substSubModules m);
625       functor = (defaultFunctor "functionTo") // { wrapped = elemType; };
626       nestedTypes.elemType = elemType;
627     };
629     # A submodule (like typed attribute set). See NixOS manual.
630     submodule = modules: submoduleWith {
631       shorthandOnlyDefinesConfig = true;
632       modules = toList modules;
633     };
635     # A module to be imported in some other part of the configuration.
636     deferredModule = deferredModuleWith { };
638     # A module to be imported in some other part of the configuration.
639     # `staticModules`' options will be added to the documentation, unlike
640     # options declared via `config`.
641     deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
642       name = "deferredModule";
643       description = "module";
644       descriptionClass = "noun";
645       check = x: isAttrs x || isFunction x || path.check x;
646       merge = loc: defs: {
647         imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
648       };
649       inherit (submoduleWith { modules = staticModules; })
650         getSubOptions
651         getSubModules;
652       substSubModules = m: deferredModuleWith (attrs // {
653         staticModules = m;
654       });
655       functor = defaultFunctor "deferredModuleWith" // {
656         type = types.deferredModuleWith;
657         payload = {
658           inherit staticModules;
659         };
660         binOp = lhs: rhs: {
661           staticModules = lhs.staticModules ++ rhs.staticModules;
662         };
663       };
664     };
666     # The type of a type!
667     optionType = mkOptionType {
668       name = "optionType";
669       description = "optionType";
670       descriptionClass = "noun";
671       check = value: value._type or null == "option-type";
672       merge = loc: defs:
673         if length defs == 1
674         then (head defs).value
675         else let
676           # Prepares the type definitions for mergeOptionDecls, which
677           # annotates submodules types with file locations
678           optionModules = map ({ value, file }:
679             {
680               _file = file;
681               # There's no way to merge types directly from the module system,
682               # but we can cheat a bit by just declaring an option with the type
683               options = lib.mkOption {
684                 type = value;
685               };
686             }
687           ) defs;
688           # Merges all the types into a single one, including submodule merging.
689           # This also propagates file information to all submodules
690           mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules);
691         in mergedOption.type;
692     };
694     submoduleWith =
695       { modules
696       , specialArgs ? {}
697       , shorthandOnlyDefinesConfig ? false
698       , description ? null
699       }@attrs:
700       let
701         inherit (lib.modules) evalModules;
703         allModules = defs: map ({ value, file }:
704           if isAttrs value && shorthandOnlyDefinesConfig
705           then { _file = file; config = value; }
706           else { _file = file; imports = [ value ]; }
707         ) defs;
709         base = evalModules {
710           inherit specialArgs;
711           modules = [{
712             # This is a work-around for the fact that some sub-modules,
713             # such as the one included in an attribute set, expects an "args"
714             # attribute to be given to the sub-module. As the option
715             # evaluation does not have any specific attribute name yet, we
716             # provide a default for the documentation and the freeform type.
717             #
718             # This is necessary as some option declaration might use the
719             # "name" attribute given as argument of the submodule and use it
720             # as the default of option declarations.
721             #
722             # We use lookalike unicode single angle quotation marks because
723             # of the docbook transformation the options receive. In all uses
724             # &gt; and &lt; wouldn't be encoded correctly so the encoded values
725             # would be used, and use of `<` and `>` would break the XML document.
726             # It shouldn't cause an issue since this is cosmetic for the manual.
727             _module.args.name = lib.mkOptionDefault "‹name›";
728           }] ++ modules;
729         };
731         freeformType = base._module.freeformType;
733         name = "submodule";
735       in
736       mkOptionType {
737         inherit name;
738         description =
739           if description != null then description
740           else freeformType.description or name;
741         check = x: isAttrs x || isFunction x || path.check x;
742         merge = loc: defs:
743           (base.extendModules {
744             modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
745             prefix = loc;
746           }).config;
747         emptyValue = { value = {}; };
748         getSubOptions = prefix: (base.extendModules
749           { inherit prefix; }).options // optionalAttrs (freeformType != null) {
750             # Expose the sub options of the freeform type. Note that the option
751             # discovery doesn't care about the attribute name used here, so this
752             # is just to avoid conflicts with potential options from the submodule
753             _freeformOptions = freeformType.getSubOptions prefix;
754           };
755         getSubModules = modules;
756         substSubModules = m: submoduleWith (attrs // {
757           modules = m;
758         });
759         nestedTypes = lib.optionalAttrs (freeformType != null) {
760           freeformType = freeformType;
761         };
762         functor = defaultFunctor name // {
763           type = types.submoduleWith;
764           payload = {
765             inherit modules specialArgs shorthandOnlyDefinesConfig description;
766           };
767           binOp = lhs: rhs: {
768             modules = lhs.modules ++ rhs.modules;
769             specialArgs =
770               let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
771               in if intersecting == {}
772               then lhs.specialArgs // rhs.specialArgs
773               else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
774             shorthandOnlyDefinesConfig =
775               if lhs.shorthandOnlyDefinesConfig == null
776               then rhs.shorthandOnlyDefinesConfig
777               else if rhs.shorthandOnlyDefinesConfig == null
778               then lhs.shorthandOnlyDefinesConfig
779               else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
780               then lhs.shorthandOnlyDefinesConfig
781               else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
782             description =
783               if lhs.description == null
784               then rhs.description
785               else if rhs.description == null
786               then lhs.description
787               else if lhs.description == rhs.description
788               then lhs.description
789               else throw "A submoduleWith option is declared multiple times with conflicting descriptions";
790           };
791         };
792       };
794     # A value from a set of allowed ones.
795     enum = values:
796       let
797         inherit (lib.lists) unique;
798         show = v:
799                if builtins.isString v then ''"${v}"''
800           else if builtins.isInt v then builtins.toString v
801           else if builtins.isBool v then boolToString v
802           else ''<${builtins.typeOf v}>'';
803       in
804       mkOptionType rec {
805         name = "enum";
806         description =
807           # Length 0 or 1 enums may occur in a design pattern with type merging
808           # where an "interface" module declares an empty enum and other modules
809           # provide implementations, each extending the enum with their own
810           # identifier.
811           if values == [] then
812             "impossible (empty enum)"
813           else if builtins.length values == 1 then
814             "value ${show (builtins.head values)} (singular enum)"
815           else
816             "one of ${concatMapStringsSep ", " show values}";
817         descriptionClass =
818           if builtins.length values < 2
819           then "noun"
820           else "conjunction";
821         check = flip elem values;
822         merge = mergeEqualOption;
823         functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
824       };
826     # Either value of type `t1` or `t2`.
827     either = t1: t2: mkOptionType rec {
828       name = "either";
829       description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
830       descriptionClass = "conjunction";
831       check = x: t1.check x || t2.check x;
832       merge = loc: defs:
833         let
834           defList = map (d: d.value) defs;
835         in
836           if   all (x: t1.check x) defList
837                then t1.merge loc defs
838           else if all (x: t2.check x) defList
839                then t2.merge loc defs
840           else mergeOneOption loc defs;
841       typeMerge = f':
842         let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
843             mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
844         in
845            if (name == f'.name) && (mt1 != null) && (mt2 != null)
846            then functor.type mt1 mt2
847            else null;
848       functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
849       nestedTypes.left = t1;
850       nestedTypes.right = t2;
851     };
853     # Any of the types in the given list
854     oneOf = ts:
855       let
856         head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
857         tail' = tail ts;
858       in foldl' either head' tail';
860     # Either value of type `coercedType` or `finalType`, the former is
861     # converted to `finalType` using `coerceFunc`.
862     coercedTo = coercedType: coerceFunc: finalType:
863       assert lib.assertMsg (coercedType.getSubModules == null)
864         "coercedTo: coercedType must not have submodules (it’s a ${
865           coercedType.description})";
866       mkOptionType rec {
867         name = "coercedTo";
868         description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it";
869         check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
870         merge = loc: defs:
871           let
872             coerceVal = val:
873               if coercedType.check val then coerceFunc val
874               else val;
875           in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
876         emptyValue = finalType.emptyValue;
877         getSubOptions = finalType.getSubOptions;
878         getSubModules = finalType.getSubModules;
879         substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
880         typeMerge = t1: t2: null;
881         functor = (defaultFunctor name) // { wrapped = finalType; };
882         nestedTypes.coercedType = coercedType;
883         nestedTypes.finalType = finalType;
884       };
886     # Augment the given type with an additional type check function.
887     addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
889   };
892 in outer_types // outer_types.types