lib.modules: init types checkAndMerge to allow adding 'valueMeta' (#391544)

This commit is contained in:
Robert Hensing
2025-08-28 14:34:31 +02:00
committed by GitHub
9 changed files with 847 additions and 81 deletions

View File

@@ -1121,6 +1121,7 @@ let
files = map (def: def.file) res.defsFinal; files = map (def: def.file) res.defsFinal;
definitionsWithLocations = res.defsFinal; definitionsWithLocations = res.defsFinal;
inherit (res) isDefined; inherit (res) isDefined;
inherit (res.checkedAndMerged) valueMeta;
# This allows options to be correctly displayed using `${options.path.to.it}` # This allows options to be correctly displayed using `${options.path.to.it}`
__toString = _: showOption loc; __toString = _: showOption loc;
}; };
@@ -1164,7 +1165,14 @@ let
# Type-check the remaining definitions, and merge them. Or throw if no definitions. # Type-check the remaining definitions, and merge them. Or throw if no definitions.
mergedValue = mergedValue =
if isDefined then if isDefined then
if all (def: type.check def.value) defsFinal then if type.merge ? v2 then
# check and merge share the same closure
# .headError is either not-present, null, or a string describing the error
if checkedAndMerged.headError or null != null then
throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError.message}"
else
checkedAndMerged.value
else if all (def: type.check def.value) defsFinal then
type.merge loc defsFinal type.merge loc defsFinal
else else
let let
@@ -1177,6 +1185,43 @@ let
throw throw
"The option `${showOption loc}' was accessed but has no value defined. Try setting the option."; "The option `${showOption loc}' was accessed but has no value defined. Try setting the option.";
checkedAndMerged =
(
# This function (which is immediately applied) checks that type.merge
# returns the proper attrset.
# Once use of the merge.v2 feature has propagated, consider removing this
# for an estimated one thousandth performance improvement (NixOS by nr.thunks).
{
headError,
value,
valueMeta,
}@args:
args
)
(
if type.merge ? v2 then
let
r = type.merge.v2 {
inherit loc;
defs = defsFinal;
};
in
r
// {
valueMeta = r.valueMeta // {
_internal = {
inherit type;
};
};
}
else
{
headError = null;
value = mergedValue;
valueMeta = { };
}
);
isDefined = defsFinal != [ ]; isDefined = defsFinal != [ ];
optionalValue = if isDefined then { value = mergedValue; } else { }; optionalValue = if isDefined then { value = mergedValue; } else { };
@@ -1586,13 +1631,11 @@ let
New option path as list of strings. New option path as list of strings.
*/ */
to, to,
/** /**
Release number of the first release that contains the rename, ignoring backports. Release number of the first release that contains the rename, ignoring backports.
Set it to the upcoming release, matching the nixpkgs/.version file. Set it to the upcoming release, matching the nixpkgs/.version file.
*/ */
sinceRelease, sinceRelease,
}: }:
doRename { doRename {
inherit from to; inherit from to;

View File

@@ -0,0 +1,379 @@
{
pkgs ? import ../.. { },
currLibPath ? ../.,
prevLibPath ? "${
pkgs.fetchFromGitHub {
owner = "nixos";
repo = "nixpkgs";
# Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544)
# Which was before the type.merge.v2 introduction
rev = "bcf94dd3f07189b7475d823c8d67d08b58289905";
hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI=";
}
}/lib",
}:
let
lib = import currLibPath;
lib_with_merge_v2 = lib;
lib_with_merge_v1 = import prevLibPath;
getMatrix =
{
getType ? null,
# If getType is set this is only used as test prefix
# And the type from getType is used
outerTypeName,
innerTypeName,
value,
testAttrs,
}:
let
evalModules.call_v1 = lib_with_merge_v1.evalModules;
evalModules.call_v2 = lib_with_merge_v2.evalModules;
outerTypes.outer_v1 = lib_with_merge_v1.types;
outerTypes.outer_v2 = lib_with_merge_v2.types;
innerTypes.inner_v1 = lib_with_merge_v1.types;
innerTypes.inner_v2 = lib_with_merge_v2.types;
in
lib.mapAttrs (
_: evalModules:
lib.mapAttrs (
_: outerTypes:
lib.mapAttrs (_: innerTypes: {
"test_${outerTypeName}_${innerTypeName}" = testAttrs // {
expr =
(evalModules {
modules = [
(m: {
options.foo = m.lib.mkOption {
type =
if getType != null then
getType outerTypes innerTypes
else
outerTypes.${outerTypeName} innerTypes.${innerTypeName};
default = value;
};
})
];
}).config.foo;
};
}) innerTypes
) outerTypes
) evalModules;
in
{
# AttrsOf string
attrsOf_str_ok = getMatrix {
outerTypeName = "attrsOf";
innerTypeName = "str";
value = {
bar = "test";
};
testAttrs = {
expected = {
bar = "test";
};
};
};
attrsOf_str_err_inner = getMatrix {
outerTypeName = "attrsOf";
innerTypeName = "str";
value = {
bar = 1; # not a string
};
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo.bar' is not of type `string'.*";
};
};
};
attrsOf_str_err_outer = getMatrix {
outerTypeName = "attrsOf";
innerTypeName = "str";
value = [ "foo" ]; # not an attrset
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo' is not of type `attribute set of string'.*";
};
};
};
# listOf string
listOf_str_ok = getMatrix {
outerTypeName = "listOf";
innerTypeName = "str";
value = [
"foo"
"bar"
];
testAttrs = {
expected = [
"foo"
"bar"
];
};
};
listOf_str_err_inner = getMatrix {
outerTypeName = "listOf";
innerTypeName = "str";
value = [
"foo"
1
]; # not a string
testAttrs = {
expectedError = {
type = "ThrownError";
msg = ''A definition for option `foo."\[definition 1-entry 2\]"' is not of type `string'.'';
};
};
};
listOf_str_err_outer = getMatrix {
outerTypeName = "listOf";
innerTypeName = "str";
value = {
foo = 42;
}; # not a list
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo' is not of type `list of string'.*";
};
};
};
attrsOf_submodule_ok = getMatrix {
getType =
a: b:
a.attrsOf (
b.submodule (m: {
options.nested = m.lib.mkOption {
type = m.lib.types.str;
};
})
);
outerTypeName = "attrsOf";
innerTypeName = "submodule";
value = {
foo = {
nested = "test1";
};
bar = {
nested = "test2";
};
};
testAttrs = {
expected = {
foo = {
nested = "test1";
};
bar = {
nested = "test2";
};
};
};
};
attrsOf_submodule_err_inner = getMatrix {
outerTypeName = "attrsOf";
innerTypeName = "submodule";
getType =
a: b:
a.attrsOf (
b.submodule (m: {
options.nested = m.lib.mkOption {
type = m.lib.types.str;
};
})
);
value = {
foo = [ 1 ]; # not a submodule
bar = {
nested = "test2";
};
};
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo.foo' is not of type `submodule'.*";
};
};
};
attrsOf_submodule_err_outer = getMatrix {
outerTypeName = "attrsOf";
innerTypeName = "submodule";
getType =
a: b:
a.attrsOf (
b.submodule (m: {
options.nested = m.lib.mkOption {
type = m.lib.types.str;
};
})
);
value = [ 123 ]; # not an attrsOf
testAttrs = {
expectedError = {
type = "ThrownError";
msg = ''A definition for option `foo' is not of type `attribute set of \(submodule\).*'';
};
};
};
# either
either_str_attrsOf_ok = getMatrix {
outerTypeName = "either";
innerTypeName = "str_or_attrsOf_str";
getType = a: b: a.either b.str (b.attrsOf a.str);
value = "string value";
testAttrs = {
expected = "string value";
};
};
either_str_attrsOf_err_1 = getMatrix {
outerTypeName = "either";
innerTypeName = "str_or_attrsOf_str";
getType = a: b: a.either b.str (b.attrsOf a.str);
value = 1;
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo' is not of type `string or attribute set of string'.*";
};
};
};
either_str_attrsOf_err_2 = getMatrix {
outerTypeName = "either";
innerTypeName = "str_or_attrsOf_str";
getType = a: b: a.either b.str (b.attrsOf a.str);
value = {
bar = 1; # not a string
};
testAttrs = {
expectedError = {
type = "ThrownError";
msg = "A definition for option `foo.bar' is not of type `string'.*";
};
};
};
# Coereced to
coerce_attrsOf_str_to_listOf_str_run = getMatrix {
outerTypeName = "coercedTo";
innerTypeName = "attrsOf_str->listOf_str";
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
value = {
bar = "test1"; # coerced to listOf string
foo = "test2"; # coerced to listOf string
};
testAttrs = {
expected = [
"test1"
"test2"
];
};
};
coerce_attrsOf_str_to_listOf_str_final = getMatrix {
outerTypeName = "coercedTo";
innerTypeName = "attrsOf_str->listOf_str";
getType = a: b: a.coercedTo (b.attrsOf b.str) (abort "This shouldnt run") (b.listOf b.str);
value = [
"test1"
"test2"
]; # already a listOf string
testAttrs = {
expected = [
"test1"
"test2"
]; # Order should be kept
};
};
coerce_attrsOf_str_to_listOf_err_coercer_input = getMatrix {
outerTypeName = "coercedTo";
innerTypeName = "attrsOf_str->listOf_str";
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
value = [
{ }
{ }
]; # not coercible to listOf string, with the given coercer
testAttrs = {
expectedError = {
type = "ThrownError";
msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*'';
};
};
};
coerce_attrsOf_str_to_listOf_err_coercer_ouput = getMatrix {
outerTypeName = "coercedTo";
innerTypeName = "attrsOf_str->listOf_str";
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
value = {
foo = {
bar = 1;
}; # coercer produces wrong type -> [ { bar = 1; } ]
};
testAttrs = {
expectedError = {
type = "ThrownError";
msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*'';
};
};
};
coerce_str_to_int_coercer_ouput = getMatrix {
outerTypeName = "coercedTo";
innerTypeName = "int->str";
getType = a: b: a.coercedTo b.int builtins.toString a.str;
value = [ ];
testAttrs = {
expectedError = {
type = "ThrownError";
msg = ''A definition for option `foo' is not of type `string or signed integer convertible to it.*'';
};
};
};
# Submodule
submodule_with_ok = getMatrix {
outerTypeName = "submoduleWith";
innerTypeName = "mixed_types";
getType =
a: b:
a.submodule (m: {
options.attrs = m.lib.mkOption {
type = b.attrsOf b.str;
};
options.list = m.lib.mkOption {
type = b.listOf b.str;
};
options.either = m.lib.mkOption {
type = b.either a.str a.int;
};
});
value = {
attrs = {
foo = "bar";
};
list = [
"foo"
"bar"
];
either = 123; # int
};
testAttrs = {
expected = {
attrs = {
foo = "bar";
};
list = [
"foo"
"bar"
];
either = 123;
};
};
};
}

View File

@@ -345,14 +345,14 @@ checkConfigOutput '^true$' "$@" ./define-module-check.nix
set -- set --
checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix
checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix
checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix checkConfigError 'A definition for option .*. is not of type .*.\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix
# Check coerced option merging. # Check coerced option merging.
checkConfigError 'The option .value. in .*/declare-coerced-value.nix. is already declared in .*/declare-coerced-value-no-default.nix.' config.value ./declare-coerced-value.nix ./declare-coerced-value-no-default.nix checkConfigError 'The option .value. in .*/declare-coerced-value.nix. is already declared in .*/declare-coerced-value-no-default.nix.' config.value ./declare-coerced-value.nix ./declare-coerced-value-no-default.nix
# Check coerced value with unsound coercion # Check coerced value with unsound coercion
checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix
checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix checkConfigError 'A definition for option .* is not of type .*.\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix
checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix
# Check `graph` attribute # Check `graph` attribute
@@ -761,6 +761,26 @@ checkConfigOutput '"bar"' config.sub.conditionalImportAsNixos.foo ./specialArgs-
checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNixos.bar ./specialArgs-class.nix checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNixos.bar ./specialArgs-class.nix
checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix
checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.nix checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.nix
# Check that some types expose the 'valueMeta'
checkConfigOutput '\{\}' options.str.valueMeta ./types-valueMeta.nix
checkConfigOutput '["foo", "bar"]' config.attrsOfResult ./types-valueMeta.nix
checkConfigOutput '2' config.listOfResult ./types-valueMeta.nix
# Check that composed types expose the 'valueMeta'
# attrsOf submodule (also on merged options,types)
checkConfigOutput '42' options.attrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix
checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix
# listOf submodule (also on merged options,types)
checkConfigOutput '42' config.listResult ./composed-types-valueMeta.nix
checkConfigOutput '42' config.mergedListResult ./composed-types-valueMeta.nix
# Add check
checkConfigOutput '^0$' config.v1CheckedPass ./add-check.nix
checkConfigError 'A definition for option .* is not of type .signed integer.*' config.v1CheckedFail ./add-check.nix
checkConfigOutput '^true$' config.v2checkedPass ./add-check.nix
checkConfigError 'A definition for option .* is not of type .attribute set of signed integer.*' config.v2checkedFail ./add-check.nix
cat <<EOF cat <<EOF
====== module tests ====== ====== module tests ======

View File

@@ -0,0 +1,36 @@
(
{ lib, ... }:
let
inherit (lib) types mkOption;
inherit (types) addCheck int attrsOf;
# type with a v1 merge
v1Type = addCheck int (v: v == 0);
# type with a v2 merge
v2Type = addCheck (attrsOf int) (v: v ? foo);
in
{
options.v1CheckedPass = mkOption {
type = v1Type;
default = 0;
};
options.v1CheckedFail = mkOption {
type = v1Type;
default = 1;
};
options.v2checkedPass = mkOption {
type = v2Type;
default = {
foo = 1;
};
# plug the value to make test script regex simple
apply = v: v.foo == 1;
};
options.v2checkedFail = mkOption {
type = v2Type;
default = { };
apply = v: lib.deepSeq v v;
};
}
)

View File

@@ -0,0 +1,75 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
attrsOfModule = mkOption {
type = types.attrsOf (
types.submodule {
options.bar = mkOption {
type = types.int;
};
}
);
};
listOfModule = mkOption {
type = types.listOf (
types.submodule {
options.bar = mkOption {
type = types.int;
};
}
);
};
in
{
imports = [
# Module A
({
options.attrsOfModule = attrsOfModule;
options.mergedAttrsOfModule = attrsOfModule;
options.listOfModule = listOfModule;
options.mergedListOfModule = listOfModule;
})
# Module B
({
options.mergedAttrsOfModule = attrsOfModule;
options.mergedListOfModule = listOfModule;
})
# Values
# It is important that the value is defined in a separate module
# Without valueMeta the actual value and sub-options wouldn't be accessible via:
# options.attrsOfModule.type.getSubOptions
({
attrsOfModule = {
foo.bar = 42;
};
mergedAttrsOfModule = {
foo.bar = 42;
};
})
(
{ options, ... }:
{
config.listOfModule = [
{
bar = 42;
}
];
config.mergedListOfModule = [
{
bar = 42;
}
];
# Result options to expose the list module to bash as plain attribute path
options.listResult = mkOption {
default = (builtins.head options.listOfModule.valueMeta.list).configuration.options.bar.value;
};
options.mergedListResult = mkOption {
default = (builtins.head options.mergedListOfModule.valueMeta.list).configuration.options.bar.value;
};
}
)
];
}

View File

@@ -0,0 +1,60 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
inherit (types)
# attrsOf uses attrsWith internally
attrsOf
listOf
submoduleOf
str
;
in
{
imports = [
(
{ options, ... }:
{
# Should have an empty valueMeta
options.str = mkOption {
type = str;
};
# Should have some valueMeta which is an attribute set of the nested valueMeta
options.attrsOf = mkOption {
type = attrsOf str;
default = {
foo = "foo";
bar = "bar";
};
};
options.attrsOfResult = mkOption {
default = builtins.attrNames options.attrsOf.valueMeta.attrs;
};
# Should have some valueMeta which is the list of the nested valueMeta of types.str
# [ {} {} ]
options.listOf = mkOption {
type = listOf str;
default = [
"foo"
"bar"
];
};
options.listOfResult = mkOption {
default = builtins.length options.listOf.valueMeta.list;
};
# Should have some valueMeta which is the submodule evaluation
# { _module, options, config, ...}
options.submoduleOf = mkOption {
type = submoduleOf {
options.str = mkOption {
type = str;
};
};
};
}
)
];
}

25
lib/tests/nix-unit.nix Normal file
View File

@@ -0,0 +1,25 @@
{
pkgs ? import ../.. { },
}:
let
prevNixpkgs = pkgs.fetchFromGitHub {
owner = "nixos";
repo = "nixpkgs";
# Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544)
# Which was before the type.merge.v2 introduction
rev = "bcf94dd3f07189b7475d823c8d67d08b58289905";
hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI=";
};
in
(pkgs.runCommand "lib-cross-eval-merge-v2"
{
nativeBuildInputs = [ pkgs.nix-unit ];
}
''
export HOME=$TMPDIR
nix-unit --eval-store "$HOME" ${./checkAndMergeCompat.nix} \
--arg currLibPath "${../.}" \
--arg prevLibPath "${prevNixpkgs}/lib"
mkdir $out
''
)

View File

@@ -29,6 +29,9 @@ in
pkgsBB.symlinkJoin { pkgsBB.symlinkJoin {
name = "nixpkgs-lib-tests"; name = "nixpkgs-lib-tests";
paths = map testWithNix nixVersions ++ [ paths = map testWithNix nixVersions ++ [
(import ./nix-unit.nix {
inherit pkgs;
})
(import ./maintainers.nix { (import ./maintainers.nix {
inherit pkgs; inherit pkgs;
lib = import ../.; lib = import ../.;

View File

@@ -20,7 +20,6 @@ let
toList toList
; ;
inherit (lib.lists) inherit (lib.lists)
all
concatLists concatLists
count count
elemAt elemAt
@@ -48,6 +47,7 @@ let
mergeOneOption mergeOneOption
mergeUniqueOption mergeUniqueOption
showFiles showFiles
showDefs
showOption showOption
; ;
inherit (lib.strings) inherit (lib.strings)
@@ -99,6 +99,13 @@ let
}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead. }is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
'' payload.elemType; '' payload.elemType;
checkDefsForError =
check: loc: defs:
let
invalidDefs = filter (def: !check def.value) defs;
in
if invalidDefs != [ ] then { message = "Definition values: ${showDefs invalidDefs}"; } else null;
outer_types = rec { outer_types = rec {
isType = type: x: (x._type or "") == type; isType = type: x: (x._type or "") == type;
@@ -705,26 +712,36 @@ let
}"; }";
descriptionClass = "composite"; descriptionClass = "composite";
check = isList; check = isList;
merge = merge = {
loc: defs: __functor =
map (x: x.value) ( self: loc: defs:
filter (x: x ? value) ( (self.v2 { inherit loc defs; }).value;
concatLists ( v2 =
imap1 ( { loc, defs }:
n: def: let
evals = filter (x: x.optionalValue ? value) (
concatLists (
imap1 ( imap1 (
m: def': n: def:
(mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [ imap1 (
{ m: def':
inherit (def) file; (mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
value = def'; {
} inherit (def) file;
]).optionalValue value = def';
) def.value }
) defs ])
) ) 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;
};
};
emptyValue = { emptyValue = {
value = [ ]; value = [ ];
}; };
@@ -801,42 +818,43 @@ let
lazy ? false, lazy ? false,
placeholder ? "name", placeholder ? "name",
}: }:
mkOptionType { mkOptionType rec {
name = if lazy then "lazyAttrsOf" else "attrsOf"; name = if lazy then "lazyAttrsOf" else "attrsOf";
description = description =
(if lazy then "lazy attribute set" else "attribute set") (if lazy then "lazy attribute set" else "attribute set")
+ " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
descriptionClass = "composite"; descriptionClass = "composite";
check = isAttrs; check = isAttrs;
merge = merge = {
if lazy then __functor =
( self: loc: defs:
# Lazy merge Function (self.v2 { inherit loc defs; }).value;
loc: defs: v2 =
zipAttrsWith { loc, defs }:
( let
name: defs: evals =
let if lazy then
merged = mergeDefinitions (loc ++ [ name ]) elemType defs; zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
# mergedValue will trigger an appropriate error when accessed else
in # Filtering makes the merge function more strict
merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue # Meaning it is less lazy
) filterAttrs (n: v: v.optionalValue ? value) (
# Push down position info. zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
(pushPositions defs) );
) in
else {
( headError = checkDefsForError check loc defs;
# Non-lazy merge Function value = mapAttrs (
loc: defs: n: v:
mapAttrs (n: v: v.value) ( if lazy then
filterAttrs (n: v: v ? value) ( v.optionalValue.value or elemType.emptyValue.value or v.mergedValue
zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue) else
# Push down position info. v.optionalValue.value
(pushPositions defs) ) evals;
) valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals;
) };
); };
emptyValue = { emptyValue = {
value = { }; value = { };
}; };
@@ -1218,6 +1236,7 @@ let
name = "submodule"; name = "submodule";
check = x: isAttrs x || isFunction x || path.check x;
in in
mkOptionType { mkOptionType {
inherit name; inherit name;
@@ -1229,13 +1248,25 @@ let
docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; }; docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; };
in in
docsEval._module.freeformType.description or name; docsEval._module.freeformType.description or name;
check = x: isAttrs x || isFunction x || path.check x; inherit check;
merge = merge = {
loc: defs: __functor =
(base.extendModules { self: loc: defs:
modules = [ { _module.args.name = last loc; } ] ++ allModules defs; (self.v2 { inherit loc defs; }).value;
prefix = loc; v2 =
}).config; { 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 = { emptyValue = {
value = { }; value = { };
}; };
@@ -1383,17 +1414,50 @@ let
}"; }";
descriptionClass = "conjunction"; descriptionClass = "conjunction";
check = x: t1.check x || t2.check x; check = x: t1.check x || t2.check x;
merge = merge = {
loc: defs: __functor =
let self: loc: defs:
defList = map (d: d.value) defs; (self.v2 { inherit loc defs; }).value;
in v2 =
if all (x: t1.check x) defList then { loc, defs }:
t1.merge loc defs let
else if all (x: t2.check x) defList then t1CheckedAndMerged =
t2.merge loc defs if t1.merge ? v2 then
else t1.merge.v2 { inherit loc defs; }
mergeOneOption loc defs; else
{
value = t1.merge loc defs;
headError = checkDefsForError t1.check loc defs;
valueMeta = { };
};
t2CheckedAndMerged =
if t2.merge ? v2 then
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 = abort "(t.merge.v2 defs).value must only be accessed when `.headError == null`. This is a bug in code that consumes a module system type.";
};
in
checkedAndMerged;
};
typeMerge = typeMerge =
f': f':
let let
@@ -1434,12 +1498,47 @@ let
optionDescriptionPhrase (class: class == "noun") coercedType optionDescriptionPhrase (class: class == "noun") coercedType
} convertible to it"; } convertible to it";
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
merge = merge = {
loc: defs: __functor =
let self: loc: defs:
coerceVal = val: if coercedType.check val then coerceFunc val else val; (self.v2 { inherit loc defs; }).value;
in v2 =
finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); { loc, defs }:
let
finalDefs = (
map (
def:
def
// {
value =
let
merged = coercedType.merge.v2 {
inherit loc;
defs = [ def ];
};
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
finalType.merge.v2 {
inherit loc;
defs = finalDefs;
}
else
{
value = finalType.merge loc finalDefs;
valueMeta = { };
headError = checkDefsForError check loc defs;
};
};
emptyValue = finalType.emptyValue; emptyValue = finalType.emptyValue;
getSubOptions = finalType.getSubOptions; getSubOptions = finalType.getSubOptions;
getSubModules = finalType.getSubModules; getSubModules = finalType.getSubModules;
@@ -1451,6 +1550,7 @@ let
nestedTypes.coercedType = coercedType; nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType; nestedTypes.finalType = finalType;
}; };
/** /**
Augment the given type with an additional type check function. Augment the given type with an additional type check function.
@@ -1459,8 +1559,33 @@ let
Fixing is not trivial, we appreciate any help! Fixing is not trivial, we appreciate any help!
::: :::
*/ */
addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; addCheck =
elemType: check:
if elemType.merge ? v2 then
elemType
// {
check = x: elemType.check x && check x;
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
orig = 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;
};
}; };
/** /**