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;
definitionsWithLocations = res.defsFinal;
inherit (res) isDefined;
inherit (res.checkedAndMerged) valueMeta;
# This allows options to be correctly displayed using `${options.path.to.it}`
__toString = _: showOption loc;
};
@@ -1164,7 +1165,14 @@ let
# Type-check the remaining definitions, and merge them. Or throw if no definitions.
mergedValue =
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
else
let
@@ -1177,6 +1185,43 @@ let
throw
"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 != [ ];
optionalValue = if isDefined then { value = mergedValue; } else { };
@@ -1586,13 +1631,11 @@ let
New option path as list of strings.
*/
to,
/**
Release number of the first release that contains the rename, ignoring backports.
Set it to the upcoming release, matching the nixpkgs/.version file.
*/
sinceRelease,
}:
doRename {
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 --
checkConfigOutput '^"42"$' config.value ./declare-coerced-value.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.
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
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
# 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 .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./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
====== 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 {
name = "nixpkgs-lib-tests";
paths = map testWithNix nixVersions ++ [
(import ./nix-unit.nix {
inherit pkgs;
})
(import ./maintainers.nix {
inherit pkgs;
lib = import ../.;

View File

@@ -20,7 +20,6 @@ let
toList
;
inherit (lib.lists)
all
concatLists
count
elemAt
@@ -48,6 +47,7 @@ let
mergeOneOption
mergeUniqueOption
showFiles
showDefs
showOption
;
inherit (lib.strings)
@@ -99,6 +99,13 @@ let
}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
'' 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 {
isType = type: x: (x._type or "") == type;
@@ -705,26 +712,36 @@ let
}";
descriptionClass = "composite";
check = isList;
merge =
loc: defs:
map (x: x.value) (
filter (x: x ? value) (
concatLists (
imap1 (
n: def:
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
evals = filter (x: x.optionalValue ? value) (
concatLists (
imap1 (
m: def':
(mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
{
inherit (def) file;
value = def';
}
]).optionalValue
) def.value
) defs
)
)
);
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;
};
};
emptyValue = {
value = [ ];
};
@@ -801,42 +818,43 @@ let
lazy ? false,
placeholder ? "name",
}:
mkOptionType {
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 = isAttrs;
merge =
if lazy then
(
# Lazy merge Function
loc: defs:
zipAttrsWith
(
name: defs:
let
merged = mergeDefinitions (loc ++ [ name ]) elemType defs;
# mergedValue will trigger an appropriate error when accessed
in
merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
)
# Push down position info.
(pushPositions defs)
)
else
(
# Non-lazy merge Function
loc: defs:
mapAttrs (n: v: v.value) (
filterAttrs (n: v: v ? value) (
zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue)
# Push down position info.
(pushPositions defs)
)
)
);
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 = { };
};
@@ -1218,6 +1236,7 @@ let
name = "submodule";
check = x: isAttrs x || isFunction x || path.check x;
in
mkOptionType {
inherit name;
@@ -1229,13 +1248,25 @@ let
docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; };
in
docsEval._module.freeformType.description or name;
check = x: isAttrs x || isFunction x || path.check x;
merge =
loc: defs:
(base.extendModules {
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
prefix = loc;
}).config;
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 = { };
};
@@ -1383,17 +1414,50 @@ let
}";
descriptionClass = "conjunction";
check = x: t1.check x || t2.check x;
merge =
loc: defs:
let
defList = map (d: d.value) defs;
in
if all (x: t1.check x) defList then
t1.merge loc defs
else if all (x: t2.check x) defList then
t2.merge loc defs
else
mergeOneOption loc defs;
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
t1CheckedAndMerged =
if t1.merge ? v2 then
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
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 =
f':
let
@@ -1434,12 +1498,47 @@ let
optionDescriptionPhrase (class: class == "noun") coercedType
} convertible to it";
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
merge =
loc: defs:
let
coerceVal = val: if coercedType.check val then coerceFunc val else val;
in
finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ 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;
getSubOptions = finalType.getSubOptions;
getSubModules = finalType.getSubModules;
@@ -1451,6 +1550,7 @@ let
nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType;
};
/**
Augment the given type with an additional type check function.
@@ -1459,8 +1559,33 @@ let
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;
};
};
/**