-
-
Notifications
You must be signed in to change notification settings - Fork 16.7k
lib.modules: init types checkAndMerge to allow adding 'valueMeta' #391544
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Apologies, but I haven’t fully grasped the specific improvements introduced by this PR or what LSPs would be able to achieve once it’s merged. Could you provide some insights with a step-by-step example in |
Of course sorry for the confusion.
Previously this option had only Now you can introspect i.e. |
c3e038c
to
860e45d
Compare
860e45d
to
6ee4232
Compare
@roberth @infinisil I've rebase this PR and added some tests |
78dc943
to
62bc75a
Compare
62bc75a
to
4685285
Compare
Needs another rebase to get rid of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very significant PR, and it needs some issues to be addressed before we can unleash this onto the ecosystem.
Small clarification on checkAndMerge
Something like checkAndMerge
is required for the (efficient) propagation of "metadata" through the type interface.
Concretely, this is because the computation work of e.g. submodule
happens through the merge
function, which can only return the removeAttrs config ["_module"]
value, as part of the submodule
contract. (otherwise we'd be spelling out things like config.fileSystems."/".config.device
all the time, which would be unpleasant, let's say)
In order to achieve the intended functionality - accessing things like options
anywhere in the configuration - we have no choice but to create an alternative to merge
.
Changing the type interface
The type interface (_type = "optionType";
) is an implicit interface that's made somewhat explicit by mkOptionType
, but this function has very limited control over the types it produces in practice, because it is fairly common practice to refine existing types into new types using the //
operator.
This creates a lack of control that makes it impossible to handle such cases in an architecturally pleasant way, and somewhat difficult to handle altogether.
In order not to break such usages, the burden of handling both the new and old methods (merge
vs checkAndMerge
) falls on the caller, as we have relatively few direct callers of the type interface, and even fewer where the distinction between these methods is all that relevant.
We should make sure that everything users want to do with //
can be done with helper functions instead, and when that's done, we can start warning about the //
usages we can detect.
When @hsjobeki and I worked on this during OceanSprint, we originally tried to have a nice and consistent type interface, which always has a checkAndMerge
function, but this would obstruct our ability to detect usages such as // { check = ... };
and handle them accordingly.
(See also the review comment on mkOptionType
)
check
functionality
This part of the solution is still underdeveloped, and needs some more exploration before I'd feel comfortable approving these changes. (It's a bit of a research-y PR)
check
serves multiple purposes.
- It is applied to each definition to make sure mistakes are reported nicely in the context of the module where the mistake was made, as opposed to just an option path
- It is used by
types.either
andtypes.coercedTo
to decide which type to delegate to.
It being a separate function is somewhat problematic
- People make wrong assumptions about it
- It can not share computation with the merge functionality
Furthermore, since the check
interface (definition value -> bool) is very limited, the error reporting is actually not great. No custom error message are possible.
This PR currently does not do much to alleviate these problems, and it may muddy the water for future such improvements. I believe this should be part of the PR's scope, unfortunately, because delaying it vastly increases the cost of doing it, as well as posing a risk of committing to a mistake that causes yet more complexity to handle all the "versions" of the type interface correctly (noting that users write their own types too, and multiple versions of lib
occasionally interact).
I believe the replacement of check
should be an attribute in the checkAndMerge
result. This allows the definition checking logic and merging logic to share a closure, which may have a positive effect on performance for the same reason as why value
and valueMeta
or combined into that attrset.
It might also positively affect taggedSubmodule. (unsure)
Due to checkAndMerge
receiving a list of definitions, this check
, this attribute needs to check a list of definitions. This should usually be done with a helper function.
Instead of returning false
, this attribute could return an error string, and null
for the current check x == true
case. (better than true
because a string isn't a valid if
condition anyway, so let's not make a wrong impression. Also nullOr str
is a good model for it, nicer than either str (enum [false])
)
Naming
We settled on checkAndMerge
for the time being, but it's somewhat misleading.
It does capture a high-level essence of the purpose of the function, but I believe we'd be better off with a more low-level name that only describes what it does, and not what its attributes do. This then helps create the correct mental model for what it is and how it behaves.
I'm thinking something like:
# old checkAndMerge
withDefinitions = loc: defs: {
# old check, but an error string instead.
# add "head" to signal that it's not a deep check, or at least usually isn't
# this also leaves space for a possible deep `error` attribute, a bit like `tryEval (deepSeq value)` but without `tryEval` and able to return an error message as a string. (out of scope for this PR)
headError = <...>;
value = <...>;
valueMeta = <...>;
}
lib/types.nix
Outdated
@@ -204,6 +205,10 @@ let | |||
# definition values and locations (e.g. [ { file = "/foo.nix"; | |||
# value = 1; } { file = "/bar.nix"; value = 2 } ]). | |||
merge ? mergeDefaultOption, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to properly detect "misuse" of // { check }
onto a checkAndMerge
-based type, we may have to make check
and merge
optional too.
Removing check
from type constructors like attrsOf
and submodule
is probably a good thing, because when users rely on the check
function of these types, they tend to be unaware that it only checks the head, i.e. isAttrs
, as the rest is (necessarily) checked in merge
, not check
.
addCheck
should then also propagate the missing-ness of check
.
4685285
to
5f4875d
Compare
This should make headError extensible other information needs to be passed This seems to improve performance slightly
ab1e9b2
to
45ed757
Compare
Some real-world testing: Mic92/dotfiles#3279 |
9144187
to
45ed757
Compare
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Just updated the performance report in the description again. I think the performance is within the acceptable margin. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked at this together with @hsjobeki again today and I'm pretty happy with this PR as is, #391544 (comment) is the only pending topic really. @roberth could you give your approval so we can merge it?
I'm still worried about
I can't easily tell whether that's all covered |
Hm... we'd need some custom test framework to test these combinations 🤔 (old) evalModules;
(new) types; And the other way around as described in the PR description. However i couldn't get this to work with our CI. We dont have the testing infrastructure yet to test this. If you prefer i could do either:
@infinisil @roberth tell me what you prefer or what you think is necessary. Thinking for myself i think going for B is the correct thing to do... |
Okay.. back from writing unit tests in nix-unit. I fetched an older lib version and run 144 Tests using the requested matrix. Here is the list of my nix-unit tests (will try to hook up with our ci now) 2 * 2 * 2 = 8 tests ; multiplied by about ~= 1 ok case and 2-4 error cases to test headError and checking infrastructure depending on the type Expand✅ attrsOf_str_err_inner.call_v1.outer_v1.inner_v1.test_attrsOf_str To ensure forward and backward stability i am looking for a way to integrate this into our CI now |
Needed to ensure backwards stability of types.merge.v2 added in NixOS#391544
Needed to ensure backwards stability of types.merge.v2 added in NixOS#391544
8aebefc
to
1fed602
Compare
Replied to the last open question here: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
This was a big one. |
Fyi: There is a minor regression on the either type. Fix is in #438558 |
This changes the
merge
behavior of some types to return anenveloping attribute set
instead of the plainvalue
Some examples:
This is related to #390952 which exposes the freeform module. After both PRs are merged the module-system should be mostly introspectable within the language and thus also by LSPs.
cc: @inclyc Do you want to integrate this into nixd after its merged?
Things done
enveloping
toattrsOf
value = ...; valueMeta = { attrs }
enveloping
tolistOf
value = ...; valueMeta = { list }
enveloping
tosubmoduleWith
value = ...; valueMeta = { config, options, .... }
types.either
types.coercedTo
checkDefsForError
TODO for proof of stability
Performance comparison (plain nixos)
Expand
I did a run with hyperfine 20 runs with 1 warmup.
CPU stats and time is noisy.
If you got any ideas how to further improve the performance, i'm more than thankfull
Cross compatibility
Expand
Test setup:
I modified the lib/tests/modules/default.nix
We also need to disable all the
valueMeta
tests, they are not expected to work if either thetype
or theevalModules
doesn't support it.We can switch around
prevLib.evalModules
andsepcialArgs.lib = thisLib
This should work for all test modules that get their
lib.types
from the modules arguments like{ lib, ... }: { ... test ... }
This allows me to run our test suite with mismatching versions.
Forward compatibility results:
Backward compatibility results:
Open questions:
addCheck
? It somehow needs to inject the checking behavior into merge...Add a 👍 reaction to pull requests you find important.