diff options
| author | Johannes Kirschbauer <hsjobeki@gmail.com> | 2026-01-08 16:49:59 +0100 |
|---|---|---|
| committer | Johannes Kirschbauer <hsjobeki@gmail.com> | 2026-01-19 13:24:05 +0100 |
| commit | 38cab2baa28a756c296a40168c6438de7a8db4f4 (patch) | |
| tree | eebc750bfde6b4e30716b793a99fff24987cf125 /lib | |
| parent | ansel: 0-unstable-2026-01-09 -> 0-unstable-2026-01-17 (#481512) (diff) | |
| download | nixpkgs-38cab2baa28a756c296a40168c6438de7a8db4f4.tar.gz | |
lib/types: unify public attribute set and ast
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/types.nix | 2885 |
1 files changed, 1443 insertions, 1442 deletions
diff --git a/lib/types.nix b/lib/types.nix index bc6e28ed9363..bd9ac93df472 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -18,6 +18,7 @@ let throwIf toDerivation toList + types ; inherit (lib.lists) concatLists @@ -87,7 +88,7 @@ let { inherit name payload; wrappedDeprecationMessage = makeWrappedDeprecationMessage payload; - type = outer_types.types.${name}; + type = types.${name}; binOp = a: b: let @@ -134,1568 +135,1568 @@ let baseType // { check = value: /* your check */; } ''; - outer_types = rec { - isType = type: x: (x._type or "") == type; +in +rec { + isType = type: x: (x._type or "") == type; - setType = - typeName: value: - value - // { - _type = typeName; - }; + setType = + typeName: value: + value + // { + _type = typeName; + }; - # Default type merging function - # takes two type functors and return the merged type - defaultTypeMerge = - f: f': - let - mergedWrapped = f.wrapped.typeMerge f'.wrapped.functor; - mergedPayload = f.binOp f.payload f'.payload; + # Default type merging function + # takes two type functors and return the merged type + defaultTypeMerge = + f: f': + let + mergedWrapped = f.wrapped.typeMerge f'.wrapped.functor; + mergedPayload = f.binOp f.payload f'.payload; - hasPayload = - assert (f'.payload != null) == (f.payload != null); - f.payload != null; - hasWrapped = - assert (f'.wrapped != null) == (f.wrapped != null); - f.wrapped != null; + hasPayload = + assert (f'.payload != null) == (f.payload != null); + f.payload != null; + hasWrapped = + assert (f'.wrapped != null) == (f.wrapped != null); + f.wrapped != null; - typeFromPayload = if mergedPayload == null then null else f.type mergedPayload; - typeFromWrapped = if mergedWrapped == null then null else f.type mergedWrapped; - in - # Abort early: cannot merge different types - if f.name != f'.name then - null - else + typeFromPayload = if mergedPayload == null then null else f.type mergedPayload; + typeFromWrapped = if mergedWrapped == null then null else f.type mergedWrapped; + in + # Abort early: cannot merge different types + if f.name != f'.name then + null + else - if hasPayload then - # Just return the payload if returning wrapped is deprecated - if f ? wrappedDeprecationMessage then - typeFromPayload - else if hasWrapped then - # Has both wrapped and payload - throw '' - Type ${f.name} defines both `functor.payload` and `functor.wrapped` at the same time, which is not supported. + if hasPayload then + # Just return the payload if returning wrapped is deprecated + if f ? wrappedDeprecationMessage then + typeFromPayload + else if hasWrapped then + # Has both wrapped and payload + throw '' + Type ${f.name} defines both `functor.payload` and `functor.wrapped` at the same time, which is not supported. - Use either `functor.payload` or `functor.wrapped` but not both. + Use either `functor.payload` or `functor.wrapped` but not both. - If your code worked before remove either `functor.wrapped` or `functor.payload` from the type definition. - '' - else - typeFromPayload - else if hasWrapped then - typeFromWrapped + If your code worked before remove either `functor.wrapped` or `functor.payload` from the type definition. + '' else - f.type; + typeFromPayload + else if hasWrapped then + typeFromWrapped + else + f.type; - # Default type functor - defaultFunctor = name: { - inherit name; - type = types.${name} or null; - wrapped = null; - payload = null; - binOp = a: b: null; + # Default type functor + defaultFunctor = name: { + inherit name; + type = lib.types.${name} or null; + wrapped = null; + payload = null; + binOp = a: b: null; + }; + + isOptionType = isType "option-type"; + mkOptionType = + { + # Human-readable representation of the type, should be equivalent to + # the type function name. + name, + # Description of the type, defined recursively by embedding the wrapped type if any. + description ? null, + # A hint for whether or not this description needs parentheses. Possible values: + # - "noun": a noun phrase + # Example description: "positive integer", + # - "conjunction": a phrase with a potentially ambiguous "or" connective + # Example description: "int or string" + # - "composite": a phrase with an "of" connective + # Example description: "list of string" + # - "nonRestrictiveClause": a noun followed by a comma and a clause + # Example description: "positive integer, meaning >0" + # See the `optionDescriptionPhrase` function. + descriptionClass ? null, + # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING! + # Function applied to each definition that must return false when a definition + # does not match the type. It should not check more than the root of the value, + # because checking nested values reduces laziness, leading to unnecessary + # infinite recursions in the module system. + # Further checks of nested values should be performed by throwing in + # the merge function. + # Strict and deep type checking can be performed by calling lib.deepSeq on + # the merged value. + # + # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change, + # https://github.com/NixOS/nixpkgs/pull/173568 and + # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this, + # https://github.com/NixOS/nixpkgs/issues/191124 and + # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore + # this disclaimer. + check ? (x: true), + # Merge a list of definitions together into a single value. + # This function is called with two arguments: the location of + # the option in the configuration as a list of strings + # (e.g. ["boot" "loader "grub" "enable"]), and a list of + # definition values and locations (e.g. [ { file = "/foo.nix"; + # value = 1; } { file = "/bar.nix"; value = 2 } ]). + merge ? mergeDefaultOption, + # Whether this type has a value representing nothingness. If it does, + # this should be a value of the form { value = <the nothing value>; } + # If it doesn't, this should be {} + # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. + emptyValue ? { }, + # Return a flat attrset of sub-options. Used to generate + # documentation. + getSubOptions ? prefix: { }, + # List of modules if any, or null if none. + getSubModules ? null, + # Function for building the same option type with a different list of + # modules. + substSubModules ? m: null, + # Function that merge type declarations. + # internal, takes a functor as argument and returns the merged type. + # returning null means the type is not mergeable + typeMerge ? defaultTypeMerge functor, + # The type functor. + # internal, representation of the type as an attribute set. + # name: name of the type + # type: type function. + # wrapped: the type wrapped in case of compound types. + # payload: values of the type, two payloads of the same type must be + # combinable with the binOp binary operation. + # binOp: binary operation that merge two payloads of the same type. + functor ? defaultFunctor name, + # The deprecation message to display when this type is used by an option + # If null, the type isn't deprecated + deprecationMessage ? null, + # The types that occur in the definition of this type. This is used to + # issue deprecation warnings recursively. Can also be used to reuse + # nested types + nestedTypes ? { }, + }: + { + _type = "option-type"; + inherit + name + check + merge + emptyValue + getSubOptions + getSubModules + substSubModules + typeMerge + deprecationMessage + nestedTypes + descriptionClass + ; + functor = + if functor ? wrappedDeprecationMessage then + functor + // { + wrapped = functor.wrappedDeprecationMessage { + loc = null; + }; + } + else + functor; + description = if description == null then name else description; }; - isOptionType = isType "option-type"; - mkOptionType = - { - # Human-readable representation of the type, should be equivalent to - # the type function name. - name, - # Description of the type, defined recursively by embedding the wrapped type if any. - description ? null, - # A hint for whether or not this description needs parentheses. Possible values: - # - "noun": a noun phrase - # Example description: "positive integer", - # - "conjunction": a phrase with a potentially ambiguous "or" connective - # Example description: "int or string" - # - "composite": a phrase with an "of" connective - # Example description: "list of string" - # - "nonRestrictiveClause": a noun followed by a comma and a clause - # Example description: "positive integer, meaning >0" - # See the `optionDescriptionPhrase` function. - descriptionClass ? null, - # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING! - # Function applied to each definition that must return false when a definition - # does not match the type. It should not check more than the root of the value, - # because checking nested values reduces laziness, leading to unnecessary - # infinite recursions in the module system. - # Further checks of nested values should be performed by throwing in - # the merge function. - # Strict and deep type checking can be performed by calling lib.deepSeq on - # the merged value. - # - # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change, - # https://github.com/NixOS/nixpkgs/pull/173568 and - # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this, - # https://github.com/NixOS/nixpkgs/issues/191124 and - # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore - # this disclaimer. - check ? (x: true), - # Merge a list of definitions together into a single value. - # This function is called with two arguments: the location of - # the option in the configuration as a list of strings - # (e.g. ["boot" "loader "grub" "enable"]), and a list of - # definition values and locations (e.g. [ { file = "/foo.nix"; - # value = 1; } { file = "/bar.nix"; value = 2 } ]). - merge ? mergeDefaultOption, - # Whether this type has a value representing nothingness. If it does, - # this should be a value of the form { value = <the nothing value>; } - # If it doesn't, this should be {} - # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. - emptyValue ? { }, - # Return a flat attrset of sub-options. Used to generate - # documentation. - getSubOptions ? prefix: { }, - # List of modules if any, or null if none. - getSubModules ? null, - # Function for building the same option type with a different list of - # modules. - substSubModules ? m: null, - # Function that merge type declarations. - # internal, takes a functor as argument and returns the merged type. - # returning null means the type is not mergeable - typeMerge ? defaultTypeMerge functor, - # The type functor. - # internal, representation of the type as an attribute set. - # name: name of the type - # type: type function. - # wrapped: the type wrapped in case of compound types. - # payload: values of the type, two payloads of the same type must be - # combinable with the binOp binary operation. - # binOp: binary operation that merge two payloads of the same type. - functor ? defaultFunctor name, - # The deprecation message to display when this type is used by an option - # If null, the type isn't deprecated - deprecationMessage ? null, - # The types that occur in the definition of this type. This is used to - # issue deprecation warnings recursively. Can also be used to reuse - # nested types - nestedTypes ? { }, - }: - { - _type = "option-type"; - inherit - name - check - merge - emptyValue - getSubOptions - getSubModules - substSubModules - typeMerge - deprecationMessage - nestedTypes - descriptionClass - ; - functor = - if functor ? wrappedDeprecationMessage then - functor - // { - wrapped = functor.wrappedDeprecationMessage { - loc = null; - }; - } - else - functor; - description = if description == null then name else description; - }; + # optionDescriptionPhrase :: (str -> bool) -> optionType -> str + # + # Helper function for producing unambiguous but readable natural language + # descriptions of types. + # + # Parameters + # + # optionDescriptionPhase unparenthesize optionType + # + # `unparenthesize`: A function from descriptionClass string to boolean. + # It must return true when the class of phrase will fit unambiguously into + # the description of the caller. + # + # `optionType`: The option type to parenthesize or not. + # The option whose description we're returning. + # + # Returns value + # + # The description of the `optionType`, with parentheses if there may be an + # ambiguity. + optionDescriptionPhrase = + unparenthesize: t: + if unparenthesize (t.descriptionClass or null) then t.description else "(${t.description})"; - # optionDescriptionPhrase :: (str -> bool) -> optionType -> str - # - # Helper function for producing unambiguous but readable natural language - # descriptions of types. - # - # Parameters - # - # optionDescriptionPhase unparenthesize optionType - # - # `unparenthesize`: A function from descriptionClass string to boolean. - # It must return true when the class of phrase will fit unambiguously into - # the description of the caller. - # - # `optionType`: The option type to parenthesize or not. - # The option whose description we're returning. - # - # Returns value - # - # The description of the `optionType`, with parentheses if there may be an - # ambiguity. - optionDescriptionPhrase = - unparenthesize: t: - if unparenthesize (t.descriptionClass or null) then t.description else "(${t.description})"; + noCheckForDocsModule = { + # When generating documentation, our goal isn't to check anything. + # Quite the opposite in fact. Generating docs is somewhat of a + # challenge, evaluating modules in a *lacking* context. Anything + # that makes the docs avoid an error is a win. + config._module.check = lib.mkForce false; + _file = "<built-in module that disables checks for the purpose of documentation generation>"; + }; - noCheckForDocsModule = { - # When generating documentation, our goal isn't to check anything. - # Quite the opposite in fact. Generating docs is somewhat of a - # challenge, evaluating modules in a *lacking* context. Anything - # that makes the docs avoid an error is a win. - config._module.check = lib.mkForce false; - _file = "<built-in module that disables checks for the purpose of documentation generation>"; - }; + # When adding new types don't forget to document them in + # nixos/doc/manual/development/option-types.section.md! - # When adding new types don't forget to document them in - # nixos/doc/manual/development/option-types.section.md! - types = rec { + raw = mkOptionType { + name = "raw"; + description = "raw value"; + descriptionClass = "noun"; + check = value: true; + merge = mergeOneOption; + }; - raw = mkOptionType { - name = "raw"; - description = "raw value"; - descriptionClass = "noun"; - check = value: true; - merge = mergeOneOption; - }; + anything = mkOptionType { + name = "anything"; + description = "anything"; + descriptionClass = "noun"; + check = value: true; + merge = + loc: defs: + let + getType = + value: if isAttrs value && isStringLike value then "stringCoercibleSet" else builtins.typeOf value; - anything = mkOptionType { - name = "anything"; - description = "anything"; - descriptionClass = "noun"; - check = value: true; - merge = - loc: defs: - let - getType = - value: if isAttrs value && isStringLike value then "stringCoercibleSet" else builtins.typeOf value; + # Returns the common type of all definitions, throws an error if they + # don't have the same type + commonType = foldl' ( + type: def: + if getType def.value == type then + type + else + throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}" + ) (getType (head defs).value) defs; - # Returns the common type of all definitions, throws an error if they - # don't have the same type - commonType = foldl' ( - type: def: - if getType def.value == type then - type - else - throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}" - ) (getType (head defs).value) defs; + mergeFunction = + { + # Recursively merge attribute sets + set = (attrsOf anything).merge; + # This is the type of packages, only accept a single definition + stringCoercibleSet = mergeOneOption; + lambda = + loc: defs: arg: + anything.merge (loc ++ [ "<function body>" ]) ( + map (def: { + file = def.file; + value = def.value arg; + }) defs + ); + # Otherwise fall back to only allowing all equal definitions + } + .${commonType} or mergeEqualOption; + in + mergeFunction loc defs; + }; - mergeFunction = - { - # Recursively merge attribute sets - set = (attrsOf anything).merge; - # This is the type of packages, only accept a single definition - stringCoercibleSet = mergeOneOption; - lambda = - loc: defs: arg: - anything.merge (loc ++ [ "<function body>" ]) ( - map (def: { - file = def.file; - value = def.value arg; - }) defs - ); - # Otherwise fall back to only allowing all equal definitions - } - .${commonType} or mergeEqualOption; - in - mergeFunction loc defs; - }; + unspecified = mkOptionType { + name = "unspecified"; + description = "unspecified value"; + descriptionClass = "noun"; + }; - unspecified = mkOptionType { - name = "unspecified"; - description = "unspecified value"; - descriptionClass = "noun"; - }; + bool = mkOptionType { + name = "bool"; + description = "boolean"; + descriptionClass = "noun"; + check = isBool; + merge = mergeEqualOption; + }; - bool = mkOptionType { - name = "bool"; - description = "boolean"; - descriptionClass = "noun"; - check = isBool; - merge = mergeEqualOption; - }; + boolByOr = mkOptionType { + name = "boolByOr"; + description = "boolean (merged using or)"; + descriptionClass = "noun"; + check = isBool; + merge = + loc: defs: + foldl' ( + result: def: + # Under the assumption that .check always runs before merge, we can assume that all defs.*.value + # have been forced, and therefore we assume we don't introduce order-dependent strictness here + result || def.value + ) false defs; + }; - boolByOr = mkOptionType { - name = "boolByOr"; - description = "boolean (merged using or)"; - descriptionClass = "noun"; - check = isBool; - merge = - loc: defs: - foldl' ( - result: def: - # Under the assumption that .check always runs before merge, we can assume that all defs.*.value - # have been forced, and therefore we assume we don't introduce order-dependent strictness here - result || def.value - ) false defs; - }; + int = mkOptionType { + name = "int"; + description = "signed integer"; + descriptionClass = "noun"; + check = isInt; + merge = mergeEqualOption; + }; - int = mkOptionType { - name = "int"; - description = "signed integer"; - descriptionClass = "noun"; - check = isInt; - merge = mergeEqualOption; - }; + # Specialized subdomains of int + ints = + let + betweenDesc = lowest: highest: "${toString lowest} and ${toString highest} (both inclusive)"; + between = + lowest: highest: + assert lib.assertMsg (lowest <= highest) "ints.between: lowest must be smaller than highest"; + addCheck int (x: x >= lowest && x <= highest) + // { + name = "intBetween"; + description = "integer between ${betweenDesc lowest highest}"; + }; + ign = + lowest: highest: name: docStart: + between lowest highest + // { + inherit name; + description = docStart + "; between ${betweenDesc lowest highest}"; + }; + unsign = + bit: range: ign 0 (range - 1) "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; + sign = + bit: range: + ign (0 - (range / 2)) ( + range / 2 - 1 + ) "signedInt${toString bit}" "${toString bit} bit signed integer"; - # Specialized subdomains of int - ints = - let - betweenDesc = lowest: highest: "${toString lowest} and ${toString highest} (both inclusive)"; - between = - lowest: highest: - assert lib.assertMsg (lowest <= highest) "ints.between: lowest must be smaller than highest"; - addCheck int (x: x >= lowest && x <= highest) - // { - name = "intBetween"; - description = "integer between ${betweenDesc lowest highest}"; - }; - ign = - lowest: highest: name: docStart: - between lowest highest - // { - inherit name; - description = docStart + "; between ${betweenDesc lowest highest}"; - }; - unsign = - bit: range: ign 0 (range - 1) "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; - sign = - bit: range: - ign (0 - (range / 2)) ( - range / 2 - 1 - ) "signedInt${toString bit}" "${toString bit} bit signed integer"; + in + { + # TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md + /** + An int with a fixed range. - in - { - # TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md - /** - An int with a fixed range. + # Example + :::{.example} + ## `lib.types.ints.between` usage example - # Example - :::{.example} - ## `lib.types.ints.between` usage example + ```nix + (ints.between 0 100).check (-1) + => false + (ints.between 0 100).check (101) + => false + (ints.between 0 0).check 0 + => true + ``` - ```nix - (ints.between 0 100).check (-1) - => false - (ints.between 0 100).check (101) - => false - (ints.between 0 0).check 0 - => true - ``` + ::: + */ + inherit between; - ::: - */ - inherit between; + unsigned = addCheck lib.types.int (x: x >= 0) // { + name = "unsignedInt"; + description = "unsigned integer, meaning >=0"; + descriptionClass = "nonRestrictiveClause"; + }; + positive = addCheck lib.types.int (x: x > 0) // { + name = "positiveInt"; + description = "positive integer, meaning >0"; + descriptionClass = "nonRestrictiveClause"; + }; + u8 = unsign 8 256; + u16 = unsign 16 65536; + # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808) + # the smallest int Nix accepts is -2^63 (-9223372036854775807) + u32 = unsign 32 4294967296; + # u64 = unsign 64 18446744073709551616; - unsigned = addCheck types.int (x: x >= 0) // { - name = "unsignedInt"; - description = "unsigned integer, meaning >=0"; - descriptionClass = "nonRestrictiveClause"; - }; - positive = addCheck types.int (x: x > 0) // { - name = "positiveInt"; - description = "positive integer, meaning >0"; - descriptionClass = "nonRestrictiveClause"; - }; - u8 = unsign 8 256; - u16 = unsign 16 65536; - # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808) - # the smallest int Nix accepts is -2^63 (-9223372036854775807) - u32 = unsign 32 4294967296; - # u64 = unsign 64 18446744073709551616; + s8 = sign 8 256; + s16 = sign 16 65536; + s32 = sign 32 4294967296; + }; - s8 = sign 8 256; - s16 = sign 16 65536; - s32 = sign 32 4294967296; - }; + # Alias of u16 for a port number + port = ints.u16; - # Alias of u16 for a port number - port = ints.u16; + float = mkOptionType { + name = "float"; + description = "floating point number"; + descriptionClass = "noun"; + check = isFloat; + merge = mergeEqualOption; + }; - float = mkOptionType { - name = "float"; - description = "floating point number"; - descriptionClass = "noun"; - check = isFloat; - merge = mergeEqualOption; + number = either int float; + + numbers = + let + betweenDesc = + lowest: highest: "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)"; + in + { + between = + lowest: highest: + assert lib.assertMsg (lowest <= highest) "numbers.between: lowest must be smaller than highest"; + addCheck number (x: x >= lowest && x <= highest) + // { + name = "numberBetween"; + description = "integer or floating point number between ${betweenDesc lowest highest}"; + }; + + nonnegative = addCheck number (x: x >= 0) // { + name = "numberNonnegative"; + description = "nonnegative integer or floating point number, meaning >=0"; + descriptionClass = "nonRestrictiveClause"; + }; + positive = addCheck number (x: x > 0) // { + name = "numberPositive"; + description = "positive integer or floating point number, meaning >0"; + descriptionClass = "nonRestrictiveClause"; }; + }; - number = either int float; + str = mkOptionType { + name = "str"; + description = "string"; + descriptionClass = "noun"; + check = isString; + merge = mergeEqualOption; + }; - numbers = - let - betweenDesc = - lowest: highest: "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)"; - in - { - between = - lowest: highest: - assert lib.assertMsg (lowest <= highest) "numbers.between: lowest must be smaller than highest"; - addCheck number (x: x >= lowest && x <= highest) - // { - name = "numberBetween"; - description = "integer or floating point number between ${betweenDesc lowest highest}"; - }; + nonEmptyStr = mkOptionType { + name = "nonEmptyStr"; + description = "non-empty string"; + descriptionClass = "noun"; + check = x: str.check x && builtins.match "[ \t\n]*" x == null; + inherit (str) merge; + }; - nonnegative = addCheck number (x: x >= 0) // { - name = "numberNonnegative"; - description = "nonnegative integer or floating point number, meaning >=0"; - descriptionClass = "nonRestrictiveClause"; - }; - positive = addCheck number (x: x > 0) // { - name = "numberPositive"; - description = "positive integer or floating point number, meaning >0"; - descriptionClass = "nonRestrictiveClause"; - }; - }; + # Allow a newline character at the end and trim it in the merge function. + singleLineStr = + let + inherit (strMatching "[^\n\r]*\n?") check merge; + in + mkOptionType { + name = "singleLineStr"; + description = "(optionally newline-terminated) single-line string"; + descriptionClass = "noun"; + inherit check; + merge = loc: defs: lib.removeSuffix "\n" (merge loc defs); + }; - str = mkOptionType { - name = "str"; - description = "string"; - descriptionClass = "noun"; - check = isString; - merge = mergeEqualOption; + strMatching = + pattern: + mkOptionType { + name = "strMatching ${escapeNixString pattern}"; + description = "string matching the pattern ${pattern}"; + descriptionClass = "noun"; + check = x: str.check x && builtins.match pattern x != null; + inherit (str) merge; + functor = defaultFunctor "strMatching" // { + type = payload: strMatching payload.pattern; + payload = { inherit pattern; }; + binOp = lhs: rhs: if lhs == rhs then lhs else null; }; + }; - nonEmptyStr = mkOptionType { - name = "nonEmptyStr"; - description = "non-empty string"; - descriptionClass = "noun"; - check = x: str.check x && builtins.match "[ \t\n]*" x == null; - inherit (str) merge; + # Merge multiple definitions by concatenating them (with the given + # separator between the values). + separatedString = + sep: + mkOptionType rec { + name = "separatedString"; + description = "strings concatenated with ${builtins.toJSON sep}"; + descriptionClass = "noun"; + check = isString; + merge = loc: defs: concatStringsSep sep (getValues defs); + functor = (defaultFunctor name) // { + payload = { inherit sep; }; + type = payload: lib.types.separatedString payload.sep; + binOp = lhs: rhs: if lhs.sep == rhs.sep then { inherit (lhs) sep; } else null; }; + }; - # Allow a newline character at the end and trim it in the merge function. - singleLineStr = - let - inherit (strMatching "[^\n\r]*\n?") check merge; - in - mkOptionType { - name = "singleLineStr"; - description = "(optionally newline-terminated) single-line string"; - descriptionClass = "noun"; - inherit check; - merge = loc: defs: lib.removeSuffix "\n" (merge loc defs); - }; + lines = separatedString "\n"; + commas = separatedString ","; + envVar = separatedString ":"; - strMatching = - pattern: - mkOptionType { - name = "strMatching ${escapeNixString pattern}"; - description = "string matching the pattern ${pattern}"; - descriptionClass = "noun"; - check = x: str.check x && builtins.match pattern x != null; - inherit (str) merge; - functor = defaultFunctor "strMatching" // { - type = payload: strMatching payload.pattern; - payload = { inherit pattern; }; - binOp = lhs: rhs: if lhs == rhs then lhs else null; - }; - }; + passwdEntry = + entryType: + addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) + // { + name = "passwdEntry ${entryType.name}"; + description = "${ + optionDescriptionPhrase (class: class == "noun") entryType + }, not containing newlines or colons"; + descriptionClass = "nonRestrictiveClause"; + }; - # Merge multiple definitions by concatenating them (with the given - # separator between the values). - separatedString = - sep: - mkOptionType rec { - name = "separatedString"; - description = "strings concatenated with ${builtins.toJSON sep}"; - descriptionClass = "noun"; - check = isString; - merge = loc: defs: concatStringsSep sep (getValues defs); - functor = (defaultFunctor name) // { - payload = { inherit sep; }; - type = payload: types.separatedString payload.sep; - binOp = lhs: rhs: if lhs.sep == rhs.sep then { inherit (lhs) sep; } else null; - }; - }; + attrs = mkOptionType { + name = "attrs"; + description = "attribute set"; + check = isAttrs; + merge = loc: foldl' (res: def: res // def.value) { }; + emptyValue = { + value = { }; + }; + }; - lines = separatedString "\n"; - commas = separatedString ","; - envVar = separatedString ":"; + fileset = mkOptionType { + name = "fileset"; + description = "fileset"; + descriptionClass = "noun"; + check = isFileset; + merge = loc: defs: unions (map (x: x.value) defs); + emptyValue.value = empty; + }; - passwdEntry = - entryType: - addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) - // { - name = "passwdEntry ${entryType.name}"; - description = "${ - optionDescriptionPhrase (class: class == "noun") entryType - }, not containing newlines or colons"; - descriptionClass = "nonRestrictiveClause"; - }; + # A package is a top-level store path (/nix/store/hash-name). This includes: + # - derivations + # - more generally, attribute sets with an `outPath` or `__toString` attribute + # pointing to a store path, e.g. flake inputs + # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo) + # - hardcoded store path literals (/nix/store/hash-foo) or strings without context + # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath. + # If you don't need a *top-level* store path, consider using pathInStore instead. + package = mkOptionType { + name = "package"; + descriptionClass = "noun"; + check = x: isDerivation x || isStorePath x; + merge = + loc: defs: + let + res = mergeOneOption loc defs; + in + if builtins.isPath res || (builtins.isString res && !builtins.hasContext res) then + toDerivation res + else + res; + }; - attrs = mkOptionType { - name = "attrs"; - description = "attribute set"; - check = isAttrs; - merge = loc: foldl' (res: def: res // def.value) { }; - emptyValue = { - value = { }; - }; - }; + shellPackage = package // { + check = x: isDerivation x && hasAttr "shellPath" x; + }; - fileset = mkOptionType { - name = "fileset"; - description = "fileset"; - descriptionClass = "noun"; - check = isFileset; - merge = loc: defs: unions (map (x: x.value) defs); - emptyValue.value = empty; - }; + pkgs = addCheck ( + unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs + // { + name = "pkgs"; + descriptionClass = "noun"; + description = "Nixpkgs package set"; + } + ) (x: (x._type or null) == "pkgs"); - # A package is a top-level store path (/nix/store/hash-name). This includes: - # - derivations - # - more generally, attribute sets with an `outPath` or `__toString` attribute - # pointing to a store path, e.g. flake inputs - # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo) - # - hardcoded store path literals (/nix/store/hash-foo) or strings without context - # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath. - # If you don't need a *top-level* store path, consider using pathInStore instead. - package = mkOptionType { - name = "package"; + path = pathWith { + absolute = true; + }; + + pathInStore = pathWith { + inStore = true; + }; + + externalPath = pathWith { + absolute = true; + inStore = false; + }; + + pathWith = + { + inStore ? null, + absolute ? null, + }: + throwIf (inStore != null && absolute != null && inStore && !absolute) + "In pathWith, inStore means the path must be absolute" + mkOptionType + { + name = "path"; + description = ( + (if absolute == null then "" else (if absolute then "absolute " else "relative ")) + + "path" + + ( + if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store") + ) + ); descriptionClass = "noun"; - check = x: isDerivation x || isStorePath x; - merge = - loc: defs: + + merge = mergeEqualOption; + functor = defaultFunctor "path" // { + type = pathWith; + payload = { inherit inStore absolute; }; + binOp = lhs: rhs: if lhs == rhs then lhs else null; + }; + + check = + x: let - res = mergeOneOption loc defs; + isInStore = lib.path.hasStorePathPrefix ( + if builtins.isPath x then + x + # Discarding string context is necessary to convert the value to + # a path and safe as the result is never used in any derivation. + else + /. + builtins.unsafeDiscardStringContext x + ); + isAbsolute = builtins.substring 0 1 (toString x) == "/"; + isExpectedType = ( + if inStore == null || inStore then isStringLike x else isString x # Do not allow a true path, which could be copied to the store later on. + ); in - if builtins.isPath res || (builtins.isString res && !builtins.hasContext res) then - toDerivation res - else - res; + isExpectedType + && (inStore == null || inStore == isInStore) + && (absolute == null || absolute == isAbsolute); }; - shellPackage = package // { - check = x: isDerivation x && hasAttr "shellPath" x; + listOf = + elemType: + mkOptionType rec { + name = "listOf"; + description = "list of ${ + optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType + }"; + descriptionClass = "composite"; + check = { + __functor = _self: isList; + isV2MergeCoherent = true; }; - - pkgs = addCheck ( - unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs - // { - name = "pkgs"; - descriptionClass = "noun"; - description = "Nixpkgs package set"; - } - ) (x: (x._type or null) == "pkgs"); - - path = pathWith { - absolute = true; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + evals = filter (x: x.optionalValue ? value) ( + concatLists ( + imap1 ( + n: def: + imap1 ( + m: def': + (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [ + { + inherit (def) file; + value = def'; + } + ]) + ) def.value + ) defs + ) + ); + in + { + headError = checkDefsForError check loc defs; + value = map (x: x.optionalValue.value or x.mergedValue) evals; + valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals; + }; }; - - pathInStore = pathWith { - inStore = true; + emptyValue = { + value = [ ]; }; - - externalPath = pathWith { - absolute = true; - inStore = false; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]); + getSubModules = elemType.getSubModules; + substSubModules = m: listOf (elemType.substSubModules m); + functor = (elemTypeFunctor name { inherit elemType; }) // { + type = payload: lib.types.listOf payload.elemType; }; + nestedTypes.elemType = elemType; + }; - pathWith = - { - inStore ? null, - absolute ? null, - }: - throwIf (inStore != null && absolute != null && inStore && !absolute) - "In pathWith, inStore means the path must be absolute" - mkOptionType - { - name = "path"; - description = ( - (if absolute == null then "" else (if absolute then "absolute " else "relative ")) - + "path" - + ( - if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store") - ) - ); - descriptionClass = "noun"; + nonEmptyListOf = + elemType: + let + list = addCheck (lib.types.listOf elemType) (l: l != [ ]); + in + list + // { + description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; + emptyValue = { }; # no .value attr, meaning unset + substSubModules = m: nonEmptyListOf (elemType.substSubModules m); + }; - merge = mergeEqualOption; - functor = defaultFunctor "path" // { - type = pathWith; - payload = { inherit inStore absolute; }; - binOp = lhs: rhs: if lhs == rhs then lhs else null; - }; + attrsOf = elemType: attrsWith { inherit elemType; }; - check = - x: - let - isInStore = lib.path.hasStorePathPrefix ( - if builtins.isPath x then - x - # Discarding string context is necessary to convert the value to - # a path and safe as the result is never used in any derivation. - else - /. + builtins.unsafeDiscardStringContext x - ); - isAbsolute = builtins.substring 0 1 (toString x) == "/"; - isExpectedType = ( - if inStore == null || inStore then isStringLike x else isString x # Do not allow a true path, which could be copied to the store later on. - ); - in - isExpectedType - && (inStore == null || inStore == isInStore) - && (absolute == null || absolute == isAbsolute); - }; + # 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 + # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an + # error that it's not defined. Use only if conditional definitions don't make sense. + lazyAttrsOf = + elemType: + attrsWith { + inherit elemType; + lazy = true; + }; - listOf = - elemType: - mkOptionType rec { - name = "listOf"; - description = "list of ${ - optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType - }"; - descriptionClass = "composite"; - check = { - __functor = _self: isList; - isV2MergeCoherent = true; + # base type for lazyAttrsOf and attrsOf + attrsWith = + let + # Push down position info. + pushPositions = map ( + def: + mapAttrs (n: v: { + inherit (def) file; + value = v; + }) def.value + ); + binOp = + lhs: rhs: + let + elemType = lhs.elemType.typeMerge rhs.elemType.functor; + lazy = if lhs.lazy == rhs.lazy then lhs.lazy else null; + placeholder = + if lhs.placeholder == rhs.placeholder then + lhs.placeholder + else if lhs.placeholder == "name" then + rhs.placeholder + else if rhs.placeholder == "name" then + lhs.placeholder + else + null; + in + if elemType == null || lazy == null || placeholder == null then + null + else + { + inherit elemType lazy placeholder; }; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - evals = filter (x: x.optionalValue ? value) ( - concatLists ( - imap1 ( - n: def: - imap1 ( - m: def': - (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [ - { - inherit (def) file; - value = def'; - } - ]) - ) def.value - ) defs - ) + in + { + elemType, + lazy ? false, + placeholder ? "name", + }: + mkOptionType rec { + name = if lazy then "lazyAttrsOf" else "attrsOf"; + description = + (if lazy then "lazy attribute set" else "attribute set") + + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + descriptionClass = "composite"; + check = { + __functor = _self: isAttrs; + isV2MergeCoherent = true; + }; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + evals = + if lazy then + zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) + else + # Filtering makes the merge function more strict + # Meaning it is less lazy + filterAttrs (n: v: v.optionalValue ? value) ( + zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) ); - in - { - headError = checkDefsForError check loc defs; - value = map (x: x.optionalValue.value or x.mergedValue) evals; - valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals; - }; - }; - emptyValue = { - value = [ ]; - }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]); - getSubModules = elemType.getSubModules; - substSubModules = m: listOf (elemType.substSubModules m); - functor = (elemTypeFunctor name { inherit elemType; }) // { - type = payload: types.listOf payload.elemType; + in + { + headError = checkDefsForError check loc defs; + value = mapAttrs ( + n: v: + if lazy then + v.optionalValue.value or elemType.emptyValue.value or v.mergedValue + else + v.optionalValue.value + ) evals; + valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals; }; - nestedTypes.elemType = elemType; - }; + }; - nonEmptyListOf = - elemType: - let - list = addCheck (types.listOf elemType) (l: l != [ ]); - in - list + emptyValue = { + value = { }; + }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<${placeholder}>" ]); + getSubModules = elemType.getSubModules; + substSubModules = + m: + attrsWith { + elemType = elemType.substSubModules m; + inherit lazy placeholder; + }; + functor = + (elemTypeFunctor "attrsWith" { + inherit elemType lazy placeholder; + }) // { - description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; - emptyValue = { }; # no .value attr, meaning unset - substSubModules = m: nonEmptyListOf (elemType.substSubModules m); + # Custom type merging required because of the "placeholder" attribute + inherit binOp; }; + nestedTypes.elemType = elemType; + }; - attrsOf = elemType: attrsWith { inherit elemType; }; - - # 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 - # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an - # error that it's not defined. Use only if conditional definitions don't make sense. - lazyAttrsOf = - elemType: - attrsWith { - inherit elemType; - lazy = true; - }; + # TODO: deprecate this in the future: + loaOf = + elemType: + lib.types.attrsOf elemType + // { + name = "loaOf"; + deprecationMessage = + "Mixing lists with attribute values is no longer" + + " possible; please use `types.attrsOf` instead. See" + + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; + nestedTypes.elemType = elemType; + }; - # base type for lazyAttrsOf and attrsOf - attrsWith = + attrTag = + tags: + let + tags_ = tags; + in + let + tags = mapAttrs ( + n: opt: + builtins.addErrorContext + "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" + ( + throwIf (opt._type or null != "option") + "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${ + if opt ? _type then + if opt._type == "option-type" then + "was a bare type, not wrapped in mkOption." + else + "was of type ${lib.strings.escapeNixString opt._type}." + else + "was not." + }" + opt + // { + declarations = + opt.declarations or ( + let + pos = builtins.unsafeGetAttrPos n tags_; + in + if pos == null then [ ] else [ pos.file ] + ); + declarationPositions = + opt.declarationPositions or ( + let + pos = builtins.unsafeGetAttrPos n tags_; + in + if pos == null then [ ] else [ pos ] + ); + } + ) + ) tags_; + choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags); + in + mkOptionType { + name = "attrTag"; + description = "attribute-tagged union with choices: ${choicesStr}"; + descriptionClass = "noun"; + getSubOptions = + prefix: mapAttrs (tagName: tagOption: tagOption // { loc = prefix ++ [ tagName ]; }) tags; + check = v: isAttrs v && length (attrNames v) == 1 && tags ? ${head (attrNames v)}; + merge = + loc: defs: let - # Push down position info. - pushPositions = map ( + choice = head (attrNames (head defs).value); + checkedValueDefs = map ( def: - mapAttrs (n: v: { - inherit (def) file; - value = v; - }) def.value - ); - binOp = - lhs: rhs: - let - elemType = lhs.elemType.typeMerge rhs.elemType.functor; - lazy = if lhs.lazy == rhs.lazy then lhs.lazy else null; - placeholder = - if lhs.placeholder == rhs.placeholder then - lhs.placeholder - else if lhs.placeholder == "name" then - rhs.placeholder - else if rhs.placeholder == "name" then - lhs.placeholder - else - null; - in - if elemType == null || lazy == null || placeholder == null then - null + assert (length (attrNames def.value)) == 1; + if (head (attrNames def.value)) != choice then + throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}." else { - inherit elemType lazy placeholder; - }; + inherit (def) file; + value = def.value.${choice}; + } + ) defs; in - { - elemType, - lazy ? false, - placeholder ? "name", - }: - mkOptionType rec { - name = if lazy then "lazyAttrsOf" else "attrsOf"; - description = - (if lazy then "lazy attribute set" else "attribute set") - + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; - descriptionClass = "composite"; - check = { - __functor = _self: isAttrs; - isV2MergeCoherent = true; - }; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - evals = - if lazy then - zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) - else - # Filtering makes the merge function more strict - # Meaning it is less lazy - filterAttrs (n: v: v.optionalValue ? value) ( - zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs) - ); - in - { - headError = checkDefsForError check loc defs; - value = mapAttrs ( - n: v: - if lazy then - v.optionalValue.value or elemType.emptyValue.value or v.mergedValue - else - v.optionalValue.value - ) evals; - valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals; - }; - }; - - emptyValue = { - value = { }; - }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<${placeholder}>" ]); - getSubModules = elemType.getSubModules; - substSubModules = - m: - attrsWith { - elemType = elemType.substSubModules m; - inherit lazy placeholder; - }; - functor = - (elemTypeFunctor "attrsWith" { - inherit elemType lazy placeholder; - }) - // { - # Custom type merging required because of the "placeholder" attribute - inherit binOp; + if tags ? ${choice} then + { + ${choice} = (lib.modules.evalOptionValue (loc ++ [ choice ]) tags.${choice} checkedValueDefs).value; + } + 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)}."; + nestedTypes = tags; + functor = defaultFunctor "attrTag" // { + type = { tags, ... }: lib.types.attrTag tags; + payload = { inherit tags; }; + binOp = + let + # Add metadata in the format that submodules work with + wrapOptionDecl = option: { + options = option; + _file = "<attrTag {...}>"; + pos = null; }; - nestedTypes.elemType = elemType; - }; - - # TODO: deprecate this in the future: - loaOf = - elemType: - types.attrsOf elemType - // { - name = "loaOf"; - deprecationMessage = - "Mixing lists with attribute values is no longer" - + " possible; please use `types.attrsOf` instead. See" - + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; - nestedTypes.elemType = elemType; - }; - - attrTag = - tags: - let - tags_ = tags; - in - let - tags = mapAttrs ( - n: opt: - builtins.addErrorContext - "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" - ( - throwIf (opt._type or null != "option") - "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${ - if opt ? _type then - if opt._type == "option-type" then - "was a bare type, not wrapped in mkOption." - else - "was of type ${lib.strings.escapeNixString opt._type}." - else - "was not." - }" - opt + in + a: b: { + tags = + a.tags + // b.tags + // mapAttrs ( + tagName: bOpt: + lib.mergeOptionDecls + # FIXME: loc is not accurate; should include prefix + # Fortunately, it's only used for error messages, where a "relative" location is kinda ok. + # It is also returned though, but use of the attribute seems rare? + [ tagName ] + [ + (wrapOptionDecl a.tags.${tagName}) + (wrapOptionDecl bOpt) + ] // { - declarations = - opt.declarations or ( - let - pos = builtins.unsafeGetAttrPos n tags_; - in - if pos == null then [ ] else [ pos.file ] - ); - declarationPositions = - opt.declarationPositions or ( - let - pos = builtins.unsafeGetAttrPos n tags_; - in - if pos == null then [ ] else [ pos ] - ); + # mergeOptionDecls is not idempotent in these attrs: + declarations = a.tags.${tagName}.declarations ++ bOpt.declarations; + declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions; } - ) - ) tags_; - choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags); - in - mkOptionType { - name = "attrTag"; - description = "attribute-tagged union with choices: ${choicesStr}"; - descriptionClass = "noun"; - getSubOptions = - prefix: mapAttrs (tagName: tagOption: tagOption // { loc = prefix ++ [ tagName ]; }) tags; - check = v: isAttrs v && length (attrNames v) == 1 && tags ? ${head (attrNames v)}; - merge = - loc: defs: - let - choice = head (attrNames (head defs).value); - checkedValueDefs = map ( - def: - assert (length (attrNames def.value)) == 1; - if (head (attrNames def.value)) != choice then - throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}." - else - { - inherit (def) file; - value = def.value.${choice}; - } - ) defs; - in - if tags ? ${choice} then - { - ${choice} = (lib.modules.evalOptionValue (loc ++ [ choice ]) tags.${choice} checkedValueDefs).value; - } - 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)}."; - nestedTypes = tags; - functor = defaultFunctor "attrTag" // { - type = { tags, ... }: types.attrTag tags; - payload = { inherit tags; }; - binOp = - let - # Add metadata in the format that submodules work with - wrapOptionDecl = option: { - options = option; - _file = "<attrTag {...}>"; - pos = null; - }; - in - a: b: { - tags = - a.tags - // b.tags - // mapAttrs ( - tagName: bOpt: - lib.mergeOptionDecls - # FIXME: loc is not accurate; should include prefix - # Fortunately, it's only used for error messages, where a "relative" location is kinda ok. - # It is also returned though, but use of the attribute seems rare? - [ tagName ] - [ - (wrapOptionDecl a.tags.${tagName}) - (wrapOptionDecl bOpt) - ] - // { - # mergeOptionDecls is not idempotent in these attrs: - declarations = a.tags.${tagName}.declarations ++ bOpt.declarations; - declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions; - } - ) (builtins.intersectAttrs a.tags b.tags); - }; + ) (builtins.intersectAttrs a.tags b.tags); }; - }; - - # A value produced by `lib.mkLuaInline` - luaInline = mkOptionType { - name = "luaInline"; - description = "inline lua"; - descriptionClass = "noun"; - check = x: x._type or null == "lua-inline"; - merge = mergeEqualOption; }; + }; - uniq = unique { message = ""; }; + # A value produced by `lib.mkLuaInline` + luaInline = mkOptionType { + name = "luaInline"; + description = "inline lua"; + descriptionClass = "noun"; + check = x: x._type or null == "lua-inline"; + merge = mergeEqualOption; + }; - unique = - { message }: - type: - mkOptionType rec { - name = "unique"; - inherit (type) description descriptionClass check; - merge = mergeUniqueOption { - inherit message; - inherit (type) merge; - }; - emptyValue = type.emptyValue; - getSubOptions = type.getSubOptions; - getSubModules = type.getSubModules; - substSubModules = m: uniq (type.substSubModules m); - functor = elemTypeFunctor name { elemType = type; } // { - type = payload: types.unique { inherit message; } payload.elemType; - }; - nestedTypes.elemType = type; - }; + uniq = unique { message = ""; }; - # Null or value of ... - nullOr = - elemType: - mkOptionType rec { - name = "nullOr"; - description = "null or ${ - optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType - }"; - descriptionClass = "conjunction"; - check = x: x == null || elemType.check x; - merge = - loc: defs: - let - nrNulls = count (def: def.value == null) defs; - in - if nrNulls == length defs then - null - else if nrNulls != 0 then - throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." - else - elemType.merge loc defs; - emptyValue = { - value = null; - }; - getSubOptions = elemType.getSubOptions; - getSubModules = elemType.getSubModules; - substSubModules = m: nullOr (elemType.substSubModules m); - functor = (elemTypeFunctor name { inherit elemType; }) // { - type = payload: types.nullOr payload.elemType; - }; - nestedTypes.elemType = elemType; - }; + unique = + { message }: + type: + mkOptionType rec { + name = "unique"; + inherit (type) description descriptionClass check; + merge = mergeUniqueOption { + inherit message; + inherit (type) merge; + }; + emptyValue = type.emptyValue; + getSubOptions = type.getSubOptions; + getSubModules = type.getSubModules; + substSubModules = m: uniq (type.substSubModules m); + functor = elemTypeFunctor name { elemType = type; } // { + type = payload: lib.types.unique { inherit message; } payload.elemType; + }; + nestedTypes.elemType = type; + }; - functionTo = - elemType: - mkOptionType { - name = "functionTo"; - description = "function that evaluates to a(n) ${ - optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType - }"; - descriptionClass = "composite"; - check = isFunction; - merge = loc: defs: { - # An argument attribute has a default when it has a default in all definitions - __functionArgs = lib.zipAttrsWith (_: lib.all (x: x)) ( - lib.map (fn: lib.functionArgs fn.value) defs - ); - __functor = - _: callerArgs: - (mergeDefinitions (loc ++ [ "<function body>" ]) elemType ( - map (fn: { - inherit (fn) file; - value = fn.value callerArgs; - }) defs - )).mergedValue; - }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]); - getSubModules = elemType.getSubModules; - substSubModules = m: functionTo (elemType.substSubModules m); - functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // { - type = payload: types.functionTo payload.elemType; - }; - nestedTypes.elemType = elemType; - }; + # Null or value of ... + nullOr = + elemType: + mkOptionType rec { + name = "nullOr"; + description = "null or ${ + optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType + }"; + descriptionClass = "conjunction"; + check = x: x == null || elemType.check x; + merge = + loc: defs: + let + nrNulls = count (def: def.value == null) defs; + in + if nrNulls == length defs then + null + else if nrNulls != 0 then + throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." + else + elemType.merge loc defs; + emptyValue = { + value = null; + }; + getSubOptions = elemType.getSubOptions; + getSubModules = elemType.getSubModules; + substSubModules = m: nullOr (elemType.substSubModules m); + functor = (elemTypeFunctor name { inherit elemType; }) // { + type = payload: lib.types.nullOr payload.elemType; + }; + nestedTypes.elemType = elemType; + }; - # A submodule (like typed attribute set). See NixOS manual. - submodule = - modules: - submoduleWith { - shorthandOnlyDefinesConfig = true; - modules = toList modules; - }; + functionTo = + elemType: + mkOptionType { + name = "functionTo"; + description = "function that evaluates to a(n) ${ + optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType + }"; + descriptionClass = "composite"; + check = isFunction; + merge = loc: defs: { + # An argument attribute has a default when it has a default in all definitions + __functionArgs = lib.zipAttrsWith (_: lib.all (x: x)) ( + lib.map (fn: lib.functionArgs fn.value) defs + ); + __functor = + _: callerArgs: + (mergeDefinitions (loc ++ [ "<function body>" ]) elemType ( + map (fn: { + inherit (fn) file; + value = fn.value callerArgs; + }) defs + )).mergedValue; + }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]); + getSubModules = elemType.getSubModules; + substSubModules = m: functionTo (elemType.substSubModules m); + functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // { + type = payload: lib.types.functionTo payload.elemType; + }; + nestedTypes.elemType = elemType; + }; - # A module to be imported in some other part of the configuration. - deferredModule = deferredModuleWith { }; + # A submodule (like typed attribute set). See NixOS manual. + submodule = + modules: + submoduleWith { + shorthandOnlyDefinesConfig = true; + modules = toList modules; + }; - # A module to be imported in some other part of the configuration. - # `staticModules`' options will be added to the documentation, unlike - # options declared via `config`. - deferredModuleWith = - attrs@{ - staticModules ? [ ], - }: - mkOptionType { - name = "deferredModule"; - description = "module"; - descriptionClass = "noun"; - check = x: isAttrs x || isFunction x || path.check x; - merge = loc: defs: { - imports = - staticModules - ++ map ( - def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value - ) defs; - }; - inherit (submoduleWith { modules = staticModules; }) - getSubOptions - getSubModules - ; - substSubModules = - m: - deferredModuleWith ( - attrs - // { - staticModules = m; - } - ); - functor = defaultFunctor "deferredModuleWith" // { - type = types.deferredModuleWith; - payload = { - inherit staticModules; - }; - binOp = lhs: rhs: { - staticModules = lhs.staticModules ++ rhs.staticModules; - }; - }; - }; + # A module to be imported in some other part of the configuration. + deferredModule = deferredModuleWith { }; - # The type of a type! - optionType = mkOptionType { - name = "optionType"; - description = "optionType"; - descriptionClass = "noun"; - check = value: value._type or null == "option-type"; - merge = - loc: defs: - if length defs == 1 then - (head defs).value - else - let - # Prepares the type definitions for mergeOptionDecls, which - # annotates submodules types with file locations - optionModules = map ( - { value, file }: - { - _file = file; - # There's no way to merge types directly from the module system, - # but we can cheat a bit by just declaring an option with the type - options = lib.mkOption { - type = value; - }; - } - ) defs; - # Merges all the types into a single one, including submodule merging. - # This also propagates file information to all submodules - mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules); - in - mergedOption.type; + # A module to be imported in some other part of the configuration. + # `staticModules`' options will be added to the documentation, unlike + # options declared via `config`. + deferredModuleWith = + attrs@{ + staticModules ? [ ], + }: + mkOptionType { + name = "deferredModule"; + description = "module"; + descriptionClass = "noun"; + check = x: isAttrs x || isFunction x || path.check x; + merge = loc: defs: { + imports = + staticModules + ++ map ( + def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value + ) defs; + }; + inherit (submoduleWith { modules = staticModules; }) + getSubOptions + getSubModules + ; + substSubModules = + m: + deferredModuleWith ( + attrs + // { + staticModules = m; + } + ); + functor = defaultFunctor "deferredModuleWith" // { + type = lib.types.deferredModuleWith; + payload = { + inherit staticModules; + }; + binOp = lhs: rhs: { + staticModules = lhs.staticModules ++ rhs.staticModules; + }; }; + }; - submoduleWith = - { - modules, - specialArgs ? { }, - shorthandOnlyDefinesConfig ? false, - description ? null, - class ? null, - }@attrs: + # The type of a type! + optionType = mkOptionType { + name = "optionType"; + description = "optionType"; + descriptionClass = "noun"; + check = value: value._type or null == "option-type"; + merge = + loc: defs: + if length defs == 1 then + (head defs).value + else let - inherit (lib.modules) evalModules; + # Prepares the type definitions for mergeOptionDecls, which + # annotates submodules types with file locations + optionModules = map ( + { value, file }: + { + _file = file; + # There's no way to merge types directly from the module system, + # but we can cheat a bit by just declaring an option with the type + options = lib.mkOption { + type = value; + }; + } + ) defs; + # Merges all the types into a single one, including submodule merging. + # This also propagates file information to all submodules + mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules); + in + mergedOption.type; + }; - allModules = - defs: - map ( - { value, file }: - if isAttrs value && shorthandOnlyDefinesConfig then - { - _file = file; - config = value; - } - else - { - _file = file; - imports = [ value ]; - } - ) defs; + submoduleWith = + { + modules, + specialArgs ? { }, + shorthandOnlyDefinesConfig ? false, + description ? null, + class ? null, + }@attrs: + let + inherit (lib.modules) evalModules; - base = evalModules { - inherit class specialArgs; - modules = [ - { - # This is a work-around for the fact that some sub-modules, - # such as the one included in an attribute set, expects an "args" - # attribute to be given to the sub-module. As the option - # evaluation does not have any specific attribute name yet, we - # provide a default for the documentation and the freeform type. - # - # This is necessary as some option declaration might use the - # "name" attribute given as argument of the submodule and use it - # as the default of option declarations. - # - # We use lookalike unicode single angle quotation marks because - # of the docbook transformation the options receive. In all uses - # > and < wouldn't be encoded correctly so the encoded values - # would be used, and use of `<` and `>` would break the XML document. - # It shouldn't cause an issue since this is cosmetic for the manual. - _module.args.name = lib.mkOptionDefault "‹name›"; - } - ] - ++ modules; - }; + allModules = + defs: + map ( + { value, file }: + if isAttrs value && shorthandOnlyDefinesConfig then + { + _file = file; + config = value; + } + else + { + _file = file; + imports = [ value ]; + } + ) defs; + + base = evalModules { + inherit class specialArgs; + modules = [ + { + # This is a work-around for the fact that some sub-modules, + # such as the one included in an attribute set, expects an "args" + # attribute to be given to the sub-module. As the option + # evaluation does not have any specific attribute name yet, we + # provide a default for the documentation and the freeform type. + # + # This is necessary as some option declaration might use the + # "name" attribute given as argument of the submodule and use it + # as the default of option declarations. + # + # We use lookalike unicode single angle quotation marks because + # of the docbook transformation the options receive. In all uses + # > and < wouldn't be encoded correctly so the encoded values + # would be used, and use of `<` and `>` would break the XML document. + # It shouldn't cause an issue since this is cosmetic for the manual. + _module.args.name = lib.mkOptionDefault "‹name›"; + } + ] + ++ modules; + }; - freeformType = base._module.freeformType; + freeformType = base._module.freeformType; - name = "submodule"; + name = "submodule"; - check = { - __functor = _self: x: isAttrs x || isFunction x || path.check x; - isV2MergeCoherent = true; + check = { + __functor = _self: x: isAttrs x || isFunction x || path.check x; + isV2MergeCoherent = true; + }; + in + mkOptionType { + inherit name; + description = + if description != null then + description + else + let + docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; }; + in + if docsEval._module.freeformType ? description then + "open ${name} of ${ + optionDescriptionPhrase ( + class: class == "noun" || class == "composite" + ) docsEval._module.freeformType + }" + else + name; + inherit check; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + configuration = base.extendModules { + modules = [ { _module.args.name = last loc; } ] ++ allModules defs; + prefix = loc; + }; + in + { + headError = checkDefsForError check loc defs; + value = configuration.config; + valueMeta = { inherit configuration; }; }; + }; + emptyValue = { + value = { }; + }; + getSubOptions = + prefix: + let + docsEval = ( + base.extendModules { + inherit prefix; + modules = [ noCheckForDocsModule ]; + } + ); + # Intentionally shadow the freeformType from the possibly *checked* + # configuration. See `noCheckForDocsModule` comment. + inherit (docsEval._module) freeformType; in - mkOptionType { - inherit name; - description = - if description != null then - description + docsEval.options + // optionalAttrs (freeformType != null) { + # Expose the sub options of the freeform type. Note that the option + # discovery doesn't care about the attribute name used here, so this + # is just to avoid conflicts with potential options from the submodule + _freeformOptions = freeformType.getSubOptions prefix; + }; + getSubModules = modules; + substSubModules = + m: + submoduleWith ( + attrs + // { + modules = m; + } + ); + nestedTypes = lib.optionalAttrs (freeformType != null) { + freeformType = freeformType; + }; + functor = defaultFunctor name // { + type = lib.types.submoduleWith; + payload = { + inherit + modules + class + specialArgs + shorthandOnlyDefinesConfig + description + ; + }; + binOp = lhs: rhs: { + class = + # `or null` was added for backwards compatibility only. `class` is + # always set in the current version of the module system. + if lhs.class or null == null then + rhs.class or null + else if rhs.class or null == null then + lhs.class or null + else if lhs.class or null == rhs.class then + lhs.class or null else - let - docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; }; - in - if docsEval._module.freeformType ? description then - "open ${name} of ${ - optionDescriptionPhrase ( - class: class == "noun" || class == "composite" - ) docsEval._module.freeformType - }" - else - name; - inherit check; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - configuration = base.extendModules { - modules = [ { _module.args.name = last loc; } ] ++ allModules defs; - prefix = loc; - }; - in - { - headError = checkDefsForError check loc defs; - value = configuration.config; - valueMeta = { inherit configuration; }; - }; - }; - emptyValue = { - value = { }; - }; - getSubOptions = - prefix: + throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; + modules = lhs.modules ++ rhs.modules; + specialArgs = let - docsEval = ( - base.extendModules { - inherit prefix; - modules = [ noCheckForDocsModule ]; - } - ); - # Intentionally shadow the freeformType from the possibly *checked* - # configuration. See `noCheckForDocsModule` comment. - inherit (docsEval._module) freeformType; + intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; in - docsEval.options - // optionalAttrs (freeformType != null) { - # Expose the sub options of the freeform type. Note that the option - # discovery doesn't care about the attribute name used here, so this - # is just to avoid conflicts with potential options from the submodule - _freeformOptions = freeformType.getSubOptions prefix; - }; - getSubModules = modules; - substSubModules = - m: - submoduleWith ( - attrs - // { - modules = m; - } - ); - nestedTypes = lib.optionalAttrs (freeformType != null) { - freeformType = freeformType; - }; - functor = defaultFunctor name // { - type = types.submoduleWith; - payload = { - inherit - modules - class - specialArgs - shorthandOnlyDefinesConfig - description - ; - }; - binOp = lhs: rhs: { - class = - # `or null` was added for backwards compatibility only. `class` is - # always set in the current version of the module system. - if lhs.class or null == null then - rhs.class or null - else if rhs.class or null == null then - lhs.class or null - else if lhs.class or null == rhs.class then - lhs.class or null - else - throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; - modules = lhs.modules ++ rhs.modules; - specialArgs = - let - intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; - in - if intersecting == { } then - lhs.specialArgs // rhs.specialArgs - else - throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; - shorthandOnlyDefinesConfig = - if lhs.shorthandOnlyDefinesConfig == null then - rhs.shorthandOnlyDefinesConfig - else if rhs.shorthandOnlyDefinesConfig == null then - lhs.shorthandOnlyDefinesConfig - else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig then - lhs.shorthandOnlyDefinesConfig - else - throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; - description = - if lhs.description == null then - rhs.description - else if rhs.description == null then - lhs.description - else if lhs.description == rhs.description then - lhs.description - else - throw "A submoduleWith option is declared multiple times with conflicting descriptions"; - }; - }; - }; - - # A value from a set of allowed ones. - enum = - values: - let - inherit (lib.lists) unique; - show = - v: - if builtins.isString v then - ''"${v}"'' - else if builtins.isInt v then - toString v - else if builtins.isBool v then - boolToString v + if intersecting == { } then + lhs.specialArgs // rhs.specialArgs else - ''<${builtins.typeOf v}>''; - in - mkOptionType rec { - name = "enum"; - description = - # Length 0 or 1 enums may occur in a design pattern with type merging - # where an "interface" module declares an empty enum and other modules - # provide implementations, each extending the enum with their own - # identifier. - if values == [ ] then - "impossible (empty enum)" - else if builtins.length values == 1 then - "value ${show (builtins.head values)} (singular enum)" + throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; + shorthandOnlyDefinesConfig = + if lhs.shorthandOnlyDefinesConfig == null then + rhs.shorthandOnlyDefinesConfig + else if rhs.shorthandOnlyDefinesConfig == null then + lhs.shorthandOnlyDefinesConfig + else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig then + lhs.shorthandOnlyDefinesConfig else - "one of ${concatMapStringsSep ", " show values}"; - descriptionClass = if builtins.length values < 2 then "noun" else "conjunction"; - check = flip elem values; - merge = mergeEqualOption; - functor = (defaultFunctor name) // { - payload = { inherit values; }; - type = payload: types.enum payload.values; - binOp = a: b: { values = unique (a.values ++ b.values); }; - }; - }; - - # Either value of type `t1` or `t2`. - either = - t1: t2: - mkOptionType rec { - name = "either"; + throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; description = - if t1.descriptionClass or null == "nonRestrictiveClause" then - # Plain, but add comma - "${t1.description}, or ${ - optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2 - }" + if lhs.description == null then + rhs.description + else if rhs.description == null then + lhs.description + else if lhs.description == rhs.description then + lhs.description else - "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${ - optionDescriptionPhrase ( - class: class == "noun" || class == "conjunction" || class == "composite" - ) t2 - }"; - descriptionClass = "conjunction"; - check = { - __functor = _self: x: t1.check x || t2.check x; - isV2MergeCoherent = true; - }; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - t1CheckedAndMerged = - if t1.merge ? v2 then - checkV2MergeCoherence loc t1 (t1.merge.v2 { inherit loc defs; }) - else - { - value = t1.merge loc defs; - headError = checkDefsForError t1.check loc defs; - valueMeta = { }; - }; - t2CheckedAndMerged = - if t2.merge ? v2 then - checkV2MergeCoherence loc t2 (t2.merge.v2 { inherit loc defs; }) - else - { - value = t2.merge loc defs; - headError = checkDefsForError t2.check loc defs; - valueMeta = { }; - }; - - checkedAndMerged = - if t1CheckedAndMerged.headError == null then - t1CheckedAndMerged - else if t2CheckedAndMerged.headError == null then - t2CheckedAndMerged - else - rec { - valueMeta = { - inherit headError; - }; - headError = { - message = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; - }; - value = lib.warn '' - One or more definitions did not pass the type-check of the 'either' type. - ${headError.message} - If `either`, `oneOf` or similar is used in freeformType, ensure that it is preceded by an 'attrsOf' such as: `freeformType = types.attrsOf (types.either t1 t2)`. - Otherwise consider using the correct type for the option `${showOption loc}`. This will be an error in Nixpkgs 26.05. - '' (mergeOneOption loc defs); - }; - in - checkedAndMerged; - }; - typeMerge = - f': - let - mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor; - mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor; - in - if (name == f'.name) && (mt1 != null) && (mt2 != null) then functor.type mt1 mt2 else null; - functor = elemTypeFunctor name { - elemType = [ - t1 - t2 - ]; - }; - nestedTypes.left = t1; - nestedTypes.right = t2; + throw "A submoduleWith option is declared multiple times with conflicting descriptions"; }; + }; + }; - # Any of the types in the given list - oneOf = - ts: - let - head' = - if ts == [ ] then throw "types.oneOf needs to get at least one type in its argument" else head ts; - tail' = tail ts; - in - foldl' either head' tail'; + # A value from a set of allowed ones. + enum = + values: + let + inherit (lib.lists) unique; + show = + v: + if builtins.isString v then + ''"${v}"'' + else if builtins.isInt v then + toString v + else if builtins.isBool v then + boolToString v + else + ''<${builtins.typeOf v}>''; + in + mkOptionType rec { + name = "enum"; + description = + # Length 0 or 1 enums may occur in a design pattern with type merging + # where an "interface" module declares an empty enum and other modules + # provide implementations, each extending the enum with their own + # identifier. + if values == [ ] then + "impossible (empty enum)" + else if builtins.length values == 1 then + "value ${show (builtins.head values)} (singular enum)" + else + "one of ${concatMapStringsSep ", " show values}"; + descriptionClass = if builtins.length values < 2 then "noun" else "conjunction"; + check = flip elem values; + merge = mergeEqualOption; + functor = (defaultFunctor name) // { + payload = { inherit values; }; + type = payload: lib.types.enum payload.values; + binOp = a: b: { values = unique (a.values ++ b.values); }; + }; + }; - # Either value of type `coercedType` or `finalType`, the former is - # converted to `finalType` using `coerceFunc`. - coercedTo = - coercedType: coerceFunc: finalType: - assert lib.assertMsg ( - coercedType.getSubModules == null - ) "coercedTo: coercedType must not have submodules (it’s a ${coercedType.description})"; - mkOptionType rec { - name = "coercedTo"; - description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${ - optionDescriptionPhrase (class: class == "noun") coercedType - } convertible to it"; - check = { - __functor = _self: x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; - isV2MergeCoherent = true; - }; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - finalDefs = ( - map ( - def: - def - // { - value = - let - merged = - if coercedType.merge ? v2 then - checkV2MergeCoherence loc coercedType ( - coercedType.merge.v2 { - inherit loc; - defs = [ def ]; - } - ) - else - null; - in - if coercedType.merge ? v2 then - if merged.headError == null then coerceFunc def.value else def.value - else if coercedType.check def.value then - coerceFunc def.value - else - def.value; - } - ) defs - ); - in - if finalType.merge ? v2 then - checkV2MergeCoherence loc finalType ( - finalType.merge.v2 { - inherit loc; - defs = finalDefs; - } - ) + # Either value of type `t1` or `t2`. + either = + t1: t2: + mkOptionType rec { + name = "either"; + description = + if t1.descriptionClass or null == "nonRestrictiveClause" then + # Plain, but add comma + "${t1.description}, or ${ + optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2 + }" + else + "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${ + optionDescriptionPhrase ( + class: class == "noun" || class == "conjunction" || class == "composite" + ) t2 + }"; + descriptionClass = "conjunction"; + check = { + __functor = _self: x: t1.check x || t2.check x; + isV2MergeCoherent = true; + }; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + t1CheckedAndMerged = + if t1.merge ? v2 then + checkV2MergeCoherence loc t1 (t1.merge.v2 { inherit loc defs; }) else { - value = finalType.merge loc finalDefs; + value = t1.merge loc defs; + headError = checkDefsForError t1.check loc defs; + valueMeta = { }; + }; + t2CheckedAndMerged = + if t2.merge ? v2 then + checkV2MergeCoherence loc t2 (t2.merge.v2 { inherit loc defs; }) + else + { + value = t2.merge loc defs; + headError = checkDefsForError t2.check loc defs; valueMeta = { }; - headError = checkDefsForError check loc defs; }; - }; - emptyValue = finalType.emptyValue; - getSubOptions = finalType.getSubOptions; - getSubModules = finalType.getSubModules; - substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); - typeMerge = t: null; - functor = (defaultFunctor name) // { - wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; }; - }; - nestedTypes.coercedType = coercedType; - nestedTypes.finalType = finalType; - }; - /** - Augment the given type with an additional type check function. + checkedAndMerged = + if t1CheckedAndMerged.headError == null then + t1CheckedAndMerged + else if t2CheckedAndMerged.headError == null then + t2CheckedAndMerged + else + rec { + valueMeta = { + inherit headError; + }; + headError = { + message = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; + }; + value = lib.warn '' + One or more definitions did not pass the type-check of the 'either' type. + ${headError.message} + If `either`, `oneOf` or similar is used in freeformType, ensure that it is preceded by an 'attrsOf' such as: `freeformType = types.attrsOf (types.either t1 t2)`. + Otherwise consider using the correct type for the option `${showOption loc}`. This will be an error in Nixpkgs 26.05. + '' (mergeOneOption loc defs); + }; + in + checkedAndMerged; + }; + typeMerge = + f': + let + mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor; + mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor; + in + if (name == f'.name) && (mt1 != null) && (mt2 != null) then functor.type mt1 mt2 else null; + functor = elemTypeFunctor name { + elemType = [ + t1 + t2 + ]; + }; + nestedTypes.left = t1; + nestedTypes.right = t2; + }; - :::{.warning} - This function has some broken behavior see: [#396021](https://github.com/NixOS/nixpkgs/issues/396021) - Fixing is not trivial, we appreciate any help! - ::: - */ - addCheck = - elemType: check: - if elemType.merge ? v2 then - elemType - // { - check = { - __functor = _self: x: elemType.check x && check x; - isV2MergeCoherent = true; - }; - merge = { - __functor = - self: loc: defs: - (self.v2 { inherit loc defs; }).value; - v2 = - { loc, defs }: - let - orig = checkV2MergeCoherence loc elemType (elemType.merge.v2 { inherit loc defs; }); - headError' = if orig.headError != null then orig.headError else checkDefsForError check loc defs; - in - orig + # Any of the types in the given list + oneOf = + ts: + let + head' = + if ts == [ ] then throw "types.oneOf needs to get at least one type in its argument" else head ts; + tail' = tail ts; + in + foldl' either head' tail'; + + # Either value of type `coercedType` or `finalType`, the former is + # converted to `finalType` using `coerceFunc`. + coercedTo = + coercedType: coerceFunc: finalType: + assert lib.assertMsg ( + coercedType.getSubModules == null + ) "coercedTo: coercedType must not have submodules (it’s a ${coercedType.description})"; + mkOptionType rec { + name = "coercedTo"; + description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${ + optionDescriptionPhrase (class: class == "noun") coercedType + } convertible to it"; + check = { + __functor = _self: x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; + isV2MergeCoherent = true; + }; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + finalDefs = ( + map ( + def: + def // { - headError = headError'; - }; + value = + let + merged = + if coercedType.merge ? v2 then + checkV2MergeCoherence loc coercedType ( + coercedType.merge.v2 { + inherit loc; + defs = [ def ]; + } + ) + else + null; + in + if coercedType.merge ? v2 then + if merged.headError == null then coerceFunc def.value else def.value + else if coercedType.check def.value then + coerceFunc def.value + else + def.value; + } + ) defs + ); + in + if finalType.merge ? v2 then + checkV2MergeCoherence loc finalType ( + finalType.merge.v2 { + inherit loc; + defs = finalDefs; + } + ) + else + { + value = finalType.merge loc finalDefs; + valueMeta = { }; + headError = checkDefsForError check loc defs; }; - } - else - elemType - // { - check = x: elemType.check x && check x; - }; + }; + emptyValue = finalType.emptyValue; + getSubOptions = finalType.getSubOptions; + getSubModules = finalType.getSubModules; + substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); + typeMerge = t: null; + functor = (defaultFunctor name) // { + wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; }; + }; + nestedTypes.coercedType = coercedType; + nestedTypes.finalType = finalType; }; - /** - Merges two option types together. + /** + Augment the given type with an additional type check function. + + :::{.warning} + This function has some broken behavior see: [#396021](https://github.com/NixOS/nixpkgs/issues/396021) + Fixing is not trivial, we appreciate any help! + ::: + */ + addCheck = + elemType: check: + if elemType.merge ? v2 then + elemType + // { + check = { + __functor = _self: x: elemType.check x && check x; + isV2MergeCoherent = true; + }; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + orig = checkV2MergeCoherence loc elemType (elemType.merge.v2 { inherit loc defs; }); + headError' = if orig.headError != null then orig.headError else checkDefsForError check loc defs; + in + orig + // { + headError = headError'; + }; + }; + } + else + elemType + // { + check = x: elemType.check x && check x; + }; + + /** + Merges two option types together. - :::{.note} - Uses the type merge function of the first type, to merge it with the second type. + :::{.note} + Uses the type merge function of the first type, to merge it with the second type. - Usually types can only be merged if they are of the same type - ::: + Usually types can only be merged if they are of the same type + ::: - # Inputs + # Inputs - : `a` (option type): The first option type. - : `b` (option type): The second option type. + : `a` (option type): The first option type. + : `b` (option type): The second option type. - # Returns + # Returns - - The merged option type. - - `{ _type = "merge-error"; error = "Cannot merge types"; }` if the types can't be merged. + - The merged option type. + - `{ _type = "merge-error"; error = "Cannot merge types"; }` if the types can't be merged. - # Examples - :::{.example} - ## `lib.types.mergeTypes` usage example - ```nix - let - enumAB = lib.types.enum ["A" "B"]; - enumXY = lib.types.enum ["X" "Y"]; - # This operation could be notated as: [ A ] | [ B ] -> [ A B ] - merged = lib.types.mergeTypes enumAB enumXY; # -> enum [ "A" "B" "X" "Y" ] - in - assert merged.check "A"; # true - assert merged.check "B"; # true - assert merged.check "X"; # true - assert merged.check "Y"; # true - merged.check "C" # false - ``` - ::: - */ - mergeTypes = - a: b: - assert isOptionType a && isOptionType b; - let - merged = a.typeMerge b.functor; - in - if merged == null then setType "merge-error" { error = "Cannot merge types"; } else merged; - }; + # Examples + :::{.example} + ## `lib.types.mergeTypes` usage example + ```nix + let + enumAB = lib.types.enum ["A" "B"]; + enumXY = lib.types.enum ["X" "Y"]; + # This operation could be notated as: [ A ] | [ B ] -> [ A B ] + merged = lib.types.mergeTypes enumAB enumXY; # -> enum [ "A" "B" "X" "Y" ] + in + assert merged.check "A"; # true + assert merged.check "B"; # true + assert merged.check "X"; # true + assert merged.check "Y"; # true + merged.check "C" # false + ``` + ::: + */ + mergeTypes = + a: b: + assert isOptionType a && isOptionType b; + let + merged = a.typeMerge b.functor; + in + if merged == null then setType "merge-error" { error = "Cannot merge types"; } else merged; -in -outer_types // outer_types.types + # TODO: Migrate usage of lib.types.types in nixpkgs + # Then add a deprecation warning + types = lib.types; +} |
