From 0b54f0517b1dba17dac51c0c0c0ae0106ebe783d Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 24 Nov 2024 16:54:20 +0100 Subject: [PATCH 1/3] lib.types.functor: use payload for all composed types --- lib/types.nix | 90 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/lib/types.nix b/lib/types.nix index 82d7425ca6439..0dbe49681fcac 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -83,47 +83,48 @@ rec { # 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; + let + mergedPayload = f.binOp f.payload f'.payload; + hasPayload = assert (f'.payload != null) == (f.payload != null); f.payload != null; in # Abort early: cannot merge different types if f.name != f'.name - then null + then null else if hasPayload then - 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. - - If your code worked before remove `functor.payload` from the type definition. - '' - else - # Has payload - if mergedPayload == null then null else f.type mergedPayload + if mergedPayload == null then null else + (f.type mergedPayload) // { + functor = f.functor // { + # Wrapped is deprecated. This attribute is added to print a meaningful error message. + wrapped = throw "oh no"; + }; + } else - if hasWrapped then - # Has wrapped - # TODO(@hsjobeki): This could also be a warning and removed in the future - if mergedWrapped == null then null else f.type mergedWrapped - else - f.type; + f.type; # Default type functor defaultFunctor = name: { inherit name; type = types.${name} or null; - wrapped = null; payload = null; + wrapped = throw "oh no"; binOp = a: b: null; }; + # Default functor for composed types + defaultComposedFunctor = name: elemType: { + inherit name; + type = { elemType, ... }: types.${name} elemType; + payload = { inherit elemType; }; + wrapped = throw "oh no"; + binOp = a: b: + let + elemType = a.elemType.typeMerge b.elemType.functor; + in + if elemType == null then null else { inherit elemType; }; + }; + isOptionType = isType "option-type"; mkOptionType = { # Human-readable representation of the type, should be equivalent to @@ -187,7 +188,6 @@ rec { # 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. @@ -570,7 +570,7 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; + functor = defaultComposedFunctor name elemType; nestedTypes.elemType = elemType; }; @@ -597,7 +597,7 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; + functor = defaultComposedFunctor name elemType; nestedTypes.elemType = elemType; }; @@ -623,7 +623,7 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); getSubModules = elemType.getSubModules; substSubModules = m: lazyAttrsOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; + functor = defaultComposedFunctor name elemType; nestedTypes.elemType = elemType; }; @@ -732,7 +732,7 @@ rec { uniq = unique { message = ""; }; - unique = { message }: type: mkOptionType rec { + unique = { message }: type: mkOptionType { name = "unique"; inherit (type) description descriptionClass check; merge = mergeUniqueOption { inherit message; inherit (type) merge; }; @@ -740,7 +740,7 @@ rec { getSubOptions = type.getSubOptions; getSubModules = type.getSubModules; substSubModules = m: uniq (type.substSubModules m); - functor = (defaultFunctor name) // { wrapped = type; }; + functor = defaultComposedFunctor "unique" type; nestedTypes.elemType = type; }; @@ -760,7 +760,7 @@ rec { getSubOptions = elemType.getSubOptions; getSubModules = elemType.getSubModules; substSubModules = m: nullOr (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; + functor = defaultComposedFunctor name elemType; nestedTypes.elemType = elemType; }; @@ -774,7 +774,7 @@ rec { getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "" ]); getSubModules = elemType.getSubModules; substSubModules = m: functionTo (elemType.substSubModules m); - functor = (defaultFunctor "functionTo") // { wrapped = elemType; }; + functor = defaultComposedFunctor "functionTo" elemType; nestedTypes.elemType = elemType; }; @@ -1005,13 +1005,29 @@ rec { then t2.merge loc defs else mergeOneOption loc defs; typeMerge = f': - let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; - mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; + 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 = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; + functor = (defaultFunctor name) // { + type = { elemType, ... }: types.${name} elemType; + payload = { + wrapped = throw "oh no"; + elemType = [ t1 t2 ]; + }; + binOp = lhs: rhs: + let + elemType = lhs.elemType.typeMerge rhs.elemType; + in + if elemType == null then + null + else + { + inherit elemType; + }; + }; nestedTypes.left = t1; nestedTypes.right = t2; }; @@ -1044,7 +1060,7 @@ rec { getSubModules = finalType.getSubModules; substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); typeMerge = t: null; - functor = (defaultFunctor name) // { wrapped = finalType; }; + functor = defaultComposedFunctor name finalType; nestedTypes.coercedType = coercedType; nestedTypes.finalType = finalType; }; From ac8670236f723ea4406f26ee75849e515f8a0d36 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 24 Nov 2024 17:47:41 +0100 Subject: [PATCH 2/3] lib.types.defaultTypeMerge: add 'loc' to default type merge --- lib/modules.nix | 3 ++- lib/types.nix | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 855ffaf25ed81..08d15804daedb 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -11,6 +11,7 @@ let concatLists concatMap concatStringsSep + defaultTypeMerge elem filter foldl' @@ -749,7 +750,7 @@ let foldl' (res: opt: let t = res.type; t' = opt.options.type; - mergedType = t.typeMerge t'.functor; + mergedType = defaultTypeMerge loc t.functor t'.functor; typesMergeable = mergedType != null; typeSet = if (bothHave "type") && typesMergeable then { type = mergedType; } diff --git a/lib/types.nix b/lib/types.nix index 82d7425ca6439..5655fc598a814 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -82,7 +82,7 @@ rec { # Default type merging function # takes two type functors and return the merged type - defaultTypeMerge = f: f': + defaultTypeMerge = loc: f: f': let mergedWrapped = f.wrapped.typeMerge f'.wrapped.functor; mergedPayload = f.binOp f.payload f'.payload; @@ -103,6 +103,8 @@ rec { Use either `functor.payload` or `functor.wrapped` but not both. If your code worked before remove `functor.payload` from the type definition. + + Location: ${showOption loc} '' else # Has payload @@ -182,7 +184,7 @@ rec { , # 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 + typeMerge ? defaultTypeMerge ["unknown location"] functor , # The type functor. # internal, representation of the type as an attribute set. # name: name of the type From 77cc156e9fed4d74306481c4bce4a47e66bb600f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 24 Nov 2024 21:54:06 +0100 Subject: [PATCH 3/3] lib.types: init types.annotated. Wrap types with arbitrary annotations Example: types.annotated { uiElement = password; } types.str --- lib/types.nix | 67 +++++++++++++++++++++++++++++++++++++++++++++++++-- test.nix | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 test.nix diff --git a/lib/types.nix b/lib/types.nix index 859782308084f..5436a40352e7d 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -730,7 +730,7 @@ rec { 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; + type = { tags }: types.attrTag tags; payload = { inherit tags; }; binOp = let @@ -872,6 +872,69 @@ rec { in mergedOption.type; }; + + # Adds annotations to the type + # Those are stored internally in `functor.payload.annotation` + # Merge behavior: + # The annotation in itself is not merged in any way. + # Both types have exact same annotation -> ok + # Only one type has annotation -> ok (the annotation is propagated) + # Two differing annotations -> fail + annotated = annotation: elemType: mkOptionType { + # Inherit all attributes from the original type + inherit (elemType) + name + description + descriptionClass + check + getSubModules + substSubModules + emptyValue + getSubOptions + merge + nestedTypes + deprecationMessage; + + # Add the annotation into the functor + functor = (fOrig: + let + origPayload = if fOrig.payload == null then {} else fOrig.payload; + + # Add the annotation + payload = origPayload // { + inherit annotation; + }; + + isTrivial = fOrig.payload == null; + origBinOp = if fOrig.payload == null then (a: b: a) else fOrig.binOp; + # Wrapp the original binOp with an 'annotation' merging one. + binOp = a: b: + ( + let + mergedAnnotation = if a.annotation == b.annotation + then a.annotation + else null; + mergedOrigPayload = (origBinOp a b); + in mergedOrigPayload // { + annotation = mergedAnnotation; + }); + + finalFunctor = fOrig // { + # This function is called by defaultTypeMerge + # with the mergedPayload to construct a new type. + type = payload: + if isTrivial then + annotated payload.annotation (types.${elemType.name}) + else + annotated payload.annotation (types.${elemType.name} payload.elemType); + + inherit binOp payload; + }; + in + finalFunctor + ) elemType.functor; + }; + submoduleWith = { modules , specialArgs ? {} @@ -1047,7 +1110,7 @@ rec { then functor.type mt1 mt2 else null; functor = (defaultFunctor name) // { - type = { elemType, ... }: types.${name} elemType; + type = { elemType }: types.${name} elemType; payload = { wrapped = deprecatedWrappedAt "Unknown location"; elemType = [ t1 t2 ]; diff --git a/test.nix b/test.nix new file mode 100644 index 0000000000000..1f9a1c41ff9b5 --- /dev/null +++ b/test.nix @@ -0,0 +1,58 @@ +let + pkgs = import ./. {} ; + inherit (pkgs) lib; + inherit (lib) types; + inherit (pkgs.lib.types) annotated; +in + { + test_has_annotation_trivial_type = let + type = annotated { test = 1;} (types.str); + in + { + expr = type.functor.payload.annotation; + expected = { test = 1; }; + }; + test_merge_annotation_trivial_type = let + typeA = annotated { test = 1;} (types.str); + typeB = annotated { test = 1;} (types.str); + mergedType = types.defaultTypeMerge "" typeA.functor typeB.functor; + in + { + expr = mergedType.functor.payload.annotation; + expected = { test = 1; }; + }; + test_has_annotation_composed_type = let + type = annotated { test = 1;} (types.attrsOf types.int); + in + { + expr = type.functor.payload.annotation; + expected = { test = 1; }; + }; + test_merge_annotation_composed_type = let + typeA = annotated { test = 1;} (types.attrsOf types.int); + typeB = annotated { test = 1;} (types.attrsOf types.int); + mergedType = types.defaultTypeMerge "" typeA.functor typeB.functor; + in + { + expr = mergedType.functor.payload.annotation; + expected = { test = 1; }; + }; + test_merge_annotation_composed_type_2 = let + typeA = annotated { test = 1;} (types.listOf types.int); + typeB = annotated { test = 1;} (types.listOf types.int); + mergedType = types.defaultTypeMerge "" typeA.functor typeB.functor; + in + { + expr = mergedType.functor.payload.annotation; + expected = { test = 1; }; + }; + test_merge_annotation_composed_type_3 = let + typeA = annotated { test = 1;} (types.enum ["foo" "bar"]); + typeB = annotated { test = 1;} (types.enum ["foo" "bar"]); + mergedType = types.defaultTypeMerge "" typeA.functor typeB.functor; + in + { + expr = mergedType.functor.payload.annotation; + expected = { test = 1; }; + }; + } \ No newline at end of file