Skip to content
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

Annotate @doc metadata when something is a guard #7893

Merged
merged 6 commits into from
Jul 15, 2018

Conversation

lackac
Copy link
Contributor

@lackac lackac commented Jul 12, 2018

Related to #7845.

This is a draft implementation after doing some experimentation with various ways of doing this. The current approach introduces the least amount of changes to make it work.

Other approaches:

  1. use :defguard/:defguardp as kind and move the @doc guard: true call to define/4. store_definition would be called with :defmacro/:defmacrop.

This would have the added benefit of having more precise error messages in the assert_* functions.

  1. use :defguard/:defguardp

This would be the most invasive change as there are a lot of places where we do something for :defmacro/:defmacrop that would have to be extended for guards. However, this would allow us to add guard: true to the metadata in Module.compile_definition_attributes/6 which could in turn allow making guard a reserved metadata key. This last benefit is lost however if we need to add @doc guard: true to builtin guards like is_binary which are defined as plain functions.

Another thing to consider is whether to make the guard metadata key reserved. If we don't that might result in confusing behaviour if someone starts using it for other purposes (however unlikely). If we do, that will require a workaround for builtin guards.

@josevalim
Copy link
Member

I like the current approach. As I don't think we should add a new kind due to the complexity of the changes (and likely breaking changes too).

Another thing to consider is whether to make the guard metadata key reserved.

I would make it reserved. The check could see if the current module is Elixir.Kernel and, if not, raise accordingly. In case we don't have the current module when we check right now, maybe we could do the check later, when we are storing the metadata in the docs table instead of when it is set.

@lackac
Copy link
Contributor Author

lackac commented Jul 13, 2018

@josevalim I agree, I wasn't comfortable starting on the more invasive changes.

I would make it reserved. The check could see if the current module is Elixir.Kernel and, if not, raise accordingly.

We have access to the module, but it's going to be a bit more complex than that. Currently we're doing the check in Module.preprocess_doc_meta/4 which is called from Module.put_attribute/6. With this approach we can't distinguish between a @doc guard: true call that comes from regular module source and one that is generated by defguard.

I'm thinking of creating an internal (but not private) function put_doc_metadata that would effectively replace the put_attribute clause that we're currently using now. This way we'll be able to call this from do_at for regular @doc keyword() calls with a flag that enables validation of reserved keys while we can also call it from other internal places to skip validation (e.g. defguard).

Do you think this would be the right approach?

@josevalim
Copy link
Member

Oh, I see. I don't like having private functions like that for Elixir only because it means folks can't have their own defguard abstraction. I think we need to bite the bullet here and not make :guard reserved then.

@lackac lackac changed the title [WIP] Annotate @doc metadata when something is a guard Annotate @doc metadata when something is a guard Jul 13, 2018
@lackac
Copy link
Contributor Author

lackac commented Jul 13, 2018

Annotated all the existing guards and added tests.

The current behaviour of IEx is that it uses defguard in the signature for a guard that is defined as a macro (all that use defguard, and Bitwise operators). I think that using defguard for functions could be misleading. Their docs still state that they are allowed in guards, so I wouldn't list guard: true in the metadata section either. We can make different decisions in ExDoc where this could be used as a grouping mechanism.

@fertapric
Copy link
Member

The current behaviour of IEx is that it uses defguard in the signature for a guard that is defined as a macro (all that use defguard, and Bitwise operators). I think that using defguard for functions could be misleading. Their docs still state that they are allowed in guards, so I wouldn't list guard: true in the metadata section either. We can make different decisions in ExDoc where this could be used as a grouping mechanism.

Maybe it's better to mark all the functions and macros that can be used as guards with @doc guard: true and use defmacro as signature for defguard?

OR...

Here is a crazy? idea

What do you think about having both @doc guard: true (used with defguard) and @doc allowed_in_guard: true for Kernel, Bitwise and custom macros?

If we go down this path, we could mark functions and macros accordingly. In addition, IEx could use defguard as signature for @doc guard: true and allowed_in_guard: true as metadata for the rest (displayed close to since: and deprecated:).

@josevalim
Copy link
Member

I think guard: true is enough to derive all of those scenarios, since custom guards can only be macros. Any function guard is built-in.

@josevalim
Copy link
Member

That said, we may go with :allowed_in_guard if we believe it is a clearer name than :guard. When it comes to the implementation though, this patch is perfect. :)

So, :guard or :allowed_in_guard?

@fertapric
Copy link
Member

fertapric commented Jul 13, 2018

I think guard: true is enough to derive all of those scenarios, since custom guards can only be macros. Any function guard is built-in.

Right. Any macro marked with @doc allowed_in_guard: true and outside Kernel (and Bitwise) should be a defguard :)

To clarify, my main concern was those macros in Kernel that are allowed in guards but are not marked with @doc allowed_in_guard: true, like Kernel.is_nil/1. I would prefer to mark those to the detriment of displaying the signature ofdefguard in IEx with defmacro :)

So, :guard or :allowed_in_guard?

I think :allowed_in_guard is clearer.

@josevalim
Copy link
Member

josevalim commented Jul 13, 2018

To clarify, my main concern was those macros in Kernel that are allowed in guards but are not marked with @doc allowed_in_guard: true, like Kernel.is_nil/1.

Good point. We should also mark any macro allowed in guards, such as is_nil/1, as @doc guard: true. It can even show up as defguard in IEx. The only reason why we can't use defguard itself is for bootstrap reasons. From the user perspective, it is exactly the same as defguard.

@lackac
Copy link
Contributor Author

lackac commented Jul 13, 2018 via email

@fertapric
Copy link
Member

@lackac we are talking about this part:

The current behaviour of IEx is that it uses defguard in the signature for a guard that is defined as a macro (all that use defguard, and Bitwise operators). I think that using defguard for functions could be misleading. Their docs still state that they are allowed in guards, so I wouldn't list guard: true in the metadata section either.

The idea is to mark any macro allowed in guards, even if IEx does not display the proper signature.

It can even show up as defguard in IEx. The only reason why we can't use defguard itself is for bootstrap reasons. From the user perspective, it is exactly the same as defguard.

However, I would go with defmacro in IEx. I think some developers might think that macros with the defguard signature in IEx are only allowed inside guards. For example, I actually checked if Elixir was raising any errors if the defguard was used outside of a guard. On the other hand, defmacro + allowed_in_guard: true (or guard: true) in IEx seems easier to understand :)

@josevalim
Copy link
Member

Here are the ones I found: in/2, and/2, or/2 and is_nil/1.

There are other things that are allowed in guards, such as sigils and left..right, but I don't think those are what people consider to be "guards". For this reason, maybe guard: true is a better name than allowed_in_guards: true, as what is allowed in guards is a bit more than the guards themselves.

I guess then it becomes slightly debatable if we should say that and, or and in should be tagged as guards. But I would say yes, especially because they return booleans.

@michalmuskala
Copy link
Member

in is not always allowed, so there's some caveats to that.

@fertapric
Copy link
Member

There are other things that are allowed in guards, such as sigils and left..right, but I don't think those are what people consider to be "guards". For this reason, maybe guard: true is a better name than allowed_in_guards: true, as what is allowed in guards is a bit more than the guards themselves.

guard: true 👌 (you changed my mind)

@josevalim
Copy link
Member

josevalim commented Jul 13, 2018

in is not always allowed, so there's some caveats to that.

@michalmuskala good point. But the error messages are good in this case so I would put it on the list for documentation purposes and let the error message guide users. I think that's better than not tagging it. Thoughts?

defp kind_to_def(:function), do: :def
defp kind_to_def(:macro), do: :defmacro
defp kind_to_def(:function, _), do: :def
defp kind_to_def(:macro, true), do: :defguard
Copy link
Member

@fertapric fertapric Jul 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last comment, promise 😅

I would go with defmacro here when displaying macros marked with @doc guard: true. As I mentioned in one of the comments, some developers might think that macros with the defguard signature are only allowed inside guards (like the macros in Bitwise and Kernel). I actually checked if Elixir was raising any errors if a guard defined with defguard was used outside of a guard.

Could defmacro + guard: true be easier to understand? (this would require to print guard: true here)

Sorry for commenting this again, I just wanted to be sure this was not overlooked :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given both defmacro and def can be used in guards, I guess showing it as guard: true is the best way to go as it would apply in both cases. 👍

@josevalim josevalim merged commit 205204d into elixir-lang:master Jul 15, 2018
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

@lackac lackac deleted the guard-metadata branch July 17, 2018 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants