-
-
Notifications
You must be signed in to change notification settings - Fork 14.2k
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
Rewrite meta-check.nix type checking logic. #37252
Conversation
f75fee9
to
ffab4b4
Compare
Okay, with the latest commit I recommend looking at |
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.
Great work on the type system, although I'm not quite sure whether this is a good idea to use for checking meta, because we already have to fight against eval time.
Apart from that, it could also make sense to replace the type checking for the NixOS module system to use this instead (as it avoids the need to use tryEval
for checking) on everything but submodules, maybe @nbp could chime in for that as well.
Other than that, I think adding another type system needs to have more eyes on this, so I'd suggest adding this as a separate pull request.
lib/types-simple.nix
Outdated
# and it returns a more or less pretty string of errors. | ||
prettyPrintErrors = | ||
let | ||
join = lib.foldl lib.concat []; |
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.
Why not concatLists
(which is a primop)?
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.
Good catch, will change.
pkgs/stdenv/generic/check-meta.nix
Outdated
checkMeta = meta: if shouldCheckMeta then lib.remove null (lib.mapAttrsToList checkMetaAttr meta) else []; | ||
checkMeta = meta: | ||
if shouldCheckMeta | ||
then lib.types-simple.checkType metaType meta |
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.
Did you actually measure the evaluation time overhead this has?
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.
Nope, how would I measure it? Maybe cc @grahamc.
Right now the checks are not enabled by default, of course that would be a final goal.
lib/types-simple.nix
Outdated
|
||
## -- HELPERS -- | ||
|
||
unimplemented = abort "unimplemented"; |
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 not used anywhere, maybe remove?
|
||
## -- TYPES -- | ||
|
||
# the type with no inhabitants (kind of useless …) |
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.
So why have it in the first place?
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.
Ah, it’s used in the tests for example: (list void)
is a type with exactly one inhabitant, []
. One wouldn’t believe it, but void is a pretty handy type to have (the description is a bit tongue-in-cheek).
}; | ||
|
||
# the type with exactly one inhabitant | ||
unit = mkScalar { |
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.
Why do we need this type?
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.
Similar to void
this doesn’t seem useful at first, but it’s even more elementar than bool
in a sense; e.g. the void
you find in languages like C(++)
is really a unit
.
A great use-case is for sum types:
let mode = sum { off = unit; level = int; };
in checkType mode { off = {}; }
which corresponds to the Haskell type:
data Mode = Off | Level Int
# list (attrs unit): | ||
# [ { a = {}; } { b = {}; } ] | ||
# [] | ||
list = t: mkRecursive { |
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.
Maybe call this listOf
so we use the same naming as with NixOS modules.
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.
I deliberately changed the names to be internally consistent; the module system has string
and str
, because string
did strange concatenation. listOf
and attrsOf
have the same history with attrs
and list
, which are not able to recursively check their contents.
Compare the correspondence table below.
# attrs (attrs string): | ||
# { foo.bar = "hello"; baz.quux = "x"; } | ||
# { x = { y = "wow"; }; } | ||
attrs = t: mkRecursive { |
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.
Same here: attrsOf
lib/types-simple.nix
Outdated
optfs = builtins.attrNames opt; | ||
vfs = builtins.attrNames v; | ||
in builtins.isAttrs v && | ||
(if opt == {} |
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.
I'd use intersectAttrs
(especially because it's a primop) here instead of converting to list and use subtractLists
.
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.
To check for then reqfs == vfs
in the line below, I’d have to use builtins.intersectAttrs v req == req
, which has to do a deep equality check for req
(or v
if I turn the arguments around).
lib/types-simple.nix
Outdated
# or even recursiveUpdate if you want to be naughty. | ||
# Hint: It’s probably better to use `productOpt` instead, | ||
# but more power to the people. | ||
defaults = t: |
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.
Hm, I don't think this has any more benefit to having to specify the defaults directly, especially if it comes to code readability. You need to look up the default here instead of just following the code you're already looking at.
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.
I implemented that as a crutch for optional fields originally, but productOpt
kind of replaced it pretty quickly. It might make sense to remove it again, yes.
Here’s a (non-exhaustive) correspondence table.
Some of the module types come from the fact that they also contain merging logic and submodule logic, which complicates the base design and lead to the deprecation of some types. |
A simple type system to check plain nix values. Please see the module comments for further explanations. Will be used to correct the buggy meta-field checks and help in sanity-checking other parts of nixpkgs.
productOpt: a product type with the possibility for optional fields defaults: generate (semi-arbitrary) default values for a given type In preparation for the rewrite of `check-meta.nix`.
The previous code used some ad-hoc attribute enumeration to check for missing fields and did not really type check the attributes, because it used `t.check` from the module system types, which only does shallow checks for nested types, e.g. just `builtins.isList` for (listOf strings). This new version uses `types-simple` to recursively check every attribute, outputting accurate information for type errors in nested fields, e.g.: Package ‘hello-2.10’ in … has an invalid meta attrset: maintainers.1.email should be: string but is: null When we set meta.maintainers [ eelco foobar ]; in `hello/default.nix` and foobar is defined as { name = "foobar"; email = null; }; in `maintainers-list.nix`. The path accurately pins down where the type check failed: In the `maintainers` field, the second element of the list (counting from 0) and the field email in that element. Further work: If an unsupported meta field is used, the type error will print the complete type of the meta product, which is quite big. The type error should show a diff of the problematic fields in that case. This is possible but not yet integrated into the type checking logic.
The meta type is changed to either a string, a license or a list of licenses, where license is an attrset with required `fullName`, `shortName` and `free` fields, and two optional fields. Every package that does not conform to this union type is fixed; I noticed that only a few packages used `list of strings`, so I corrected them and removed the alternative.
Especially document the inner workings of the `checkType` function.
`defaults` originally only was a crutch to make products accept optional attributes shallowly, but that has been replaced by `productOpt`, so we don’t need it anymore. Also a few other changes based on input by @aszlig.
f0e032b
to
eefcd09
Compare
Okay, rebase onto current master after the changes in @Ericson2314 I noticed you create a lot of types in |
# TODO: use the types from lib/systems/parse.nix | ||
# any should be lib.systems.parsed.types.system | ||
system = any; | ||
platform = union [ string any ]; |
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.
@Ericson2314 This is where I stub out the platform types at the moment.
`restrict` is able to restrict the inhabitants of a type, meaning only a subset of a type is allowed, the new type is “smaller”, but still gives nice type errors if a nested value has the wrong type. See the tests for some usage examples.
e6f1908
to
85f28ff
Compare
Update: |
@@ -31,6 +31,9 @@ let | |||
options = callLibs ./options.nix; | |||
types = callLibs ./types.nix; | |||
|
|||
# type checking plain nix values | |||
types-simple = callLibs ./types-simple.nix; |
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.
Please use camelCase for attribute names.
What is the memory / evaluation time impact of this? |
}; | ||
|
||
|
||
# -- Products -- |
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.
What are "products"? Seems like a pretty strange terminology.
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.
types-simple
is essentially defining algebraic data types, like ML or Haskell have; "products" is the standard terminology for tuple- or record-like things in this setting.
A couple of clarifications.
In
I think `types-simple` should replace `types`
I mean that either `types-simple` should expose the merging NixOS modules would use, or `types` should `types-simple` for type checking.
In
without duplicating already inferable information
and related I mean the following kind of duplication
```imaginary-typed-nix
option :: builtintypes.str
option = mkOption {
merge = types.str.merge;
};
```
Similarly, in
a semantically very different nix
and related I also mean that it's unclear how you would typecheck
```nix
{
require = [
<module that defines `option`>
];
config = {
option = value;
};
}
```
without merging NixOS modules and nix (currently non-existent) modules into a single concept.
Also, an idea I wanted to use when I planned implementing pretty much the same thing: I planned to make `bool` into a non-mergable bool and split other uses into `types.any` and `types.all` since `bool` has two different monoids. E.g. you would then use `all` for normal `enable` options (because if somebody disabled it, they probably know what they are doing, why make them use `mkForce`?) and `any` for security-related options (because to make things insecure everyone should agree).
|
Ah, the |
ping! Progress? Do you have an updated/rebased version of this to play with? |
@oxij I have just pushed a branch which does away with the in-file changes of I intend to commit all improvements to that branch (types-simple-standalone) first. |
`names` was originally intended to be able to match on the specific instance of a type, but it breaks the abstraction in a way. Since the function that used it was removed, we can remove the name attributes as well.
@Profpatsch awesome, thanks! |
Wow, this is great work ! I really like having the typeckecking logic separate from the merging logic. Is there a plan to make nixos types use this for typechecking under the hood ? |
@Profpatsch this is perhaps somewhat off-topic, but I read through From the context, I gather that the design is based on ideas from category theory, and would (I assume) be easily comprehensible to people familiar with it. As a result, I am curious: are there other benefits in this specific instance to keeping the implementation so close to the abstract design? Those comments notwithstanding, I still found it easier to understand than the existing module type system, and having a self-contained type system in place would be very useful. |
No, not really, it’s from the classic bananas & barbed wire paper. You can find an explanation here: http://reasonablypolymorphic.com/blog/recursion-schemes/ |
…g` of NixOS to it This types `nixpkgs.config` of NixOS with `types.opaque` type and applies the resulting evaluated value to the newly introduced `configs` argument of `pkgs`. The new `configs` argument is like the old `config` argument (still available for convenience) but a list. This design was chosen because we can't just make a submodule out of `pkgs/top-level/config.nix` in NixOS because - firstly, we would have to duplicate all the magic `pkgs/top-level/default.nix` does to `config` in NixOS, - secondly, in principle, we might not have all the arguments needed by `pkgs/top-level/config.nix` in NixOS because some of them can be computed internally by `pkgs` (this is not a problem now but it will be a problem in use-flags patchset that comes after), thus all of those computations will need to be duplicated too, - thirdly, doing it this way keeps NixOS and pkgs separated, so that, in principle, one could replace NixOS with an alternative thing without duplicating all of the above once again and/or one could use separate module and type systems between NixOS and pkgs (`types.opaque` is trivial to implement in any type system and pkgs can use "types-simple" of NixOS#37252 today, if we were to wish it so). Note that since this design removes the need to do any preprocessing of `nixpkgs.config` in NixOS all the ad-hoc merging code was removed. Yay!
…g` of NixOS to it This types `nixpkgs.config` of NixOS with `types.opaque` type and applies the resulting evaluated value to the newly introduced `configs` argument of `pkgs`. The new `configs` argument is like the old `config` argument (still available for convenience) but a list. This design was chosen because we can't just make a submodule out of `pkgs/top-level/config.nix` in NixOS because - firstly, we would have to duplicate all the magic `pkgs/top-level/default.nix` does to `config` in NixOS, - secondly, in principle, we might not have all the arguments needed by `pkgs/top-level/config.nix` in NixOS because some of them can be computed internally by `pkgs` (this is not a problem now but it will be a problem in use-flags patchset that comes after), thus all of those computations will need to be duplicated too, - thirdly, doing it this way keeps NixOS and pkgs separated, so that, in principle, one could replace NixOS with an alternative thing without duplicating all of the above once again and/or one could use separate module and type systems between NixOS and pkgs (`types.opaque` is trivial to implement in any type system and pkgs can use "types-simple" of NixOS#37252 today, if we were to wish it so). Note that since this design removes the need to do any preprocessing of `nixpkgs.config` in NixOS all the ad-hoc merging code was removed. Yay!
Are there any updates on this pull request, please? |
I still think it’s valuable, but I don’t know of any way out of the “we don’t want two type systems in nixpkgs” issue. |
Thank you for your contributions.
|
Description of the main commit:
Please go trough the commits one-by-one, since they all add some pretty different changes and are built on each other as single units.Adding the type library might be a bit controversial, but I hope I have provided enough incentive in the docstrings and the commit messages to persuade you that it is a good addition.
Meta can be checked with this invocation from the ofborg readme.