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

ArgValue derive #2762

Merged
merged 10 commits into from
Oct 1, 2021
Merged

ArgValue derive #2762

merged 10 commits into from
Oct 1, 2021

Conversation

ModProg
Copy link
Contributor

@ModProg ModProg commented Sep 9, 2021

Support ArgValue in ArgEnum derive macro.

merge after: #2758

fully closes: #2756
fully closes: #2731

For reference: #2758 (comment)

@ModProg ModProg mentioned this pull request Sep 9, 2021
src/derive.rs Outdated Show resolved Hide resolved
@ModProg ModProg force-pushed the ArgValue-derive branch 3 times, most recently from 0877e5a to 2d4abfa Compare September 12, 2021 23:50
@ModProg

This comment has been minimized.

@epage
Copy link
Member

epage commented Sep 16, 2021

Bleh, the pain of sharing code between all macros. alias is a method for Args but is purely magic for ArgEnum. Its ugly but we could add a Attrs::arg_enum_methods function that filters out alias.

@ModProg

This comment has been minimized.

@ModProg
Copy link
Contributor Author

ModProg commented Sep 18, 2021

@epage Or add a parameter filter_neg:[&str] to methods.
I implemented this now using a new method: field_methods_filtered.

I would like some feedback on changes required for a merge. As IMO all features are implemented now.

@epage
Copy link
Member

epage commented Sep 19, 2021

Honestly, I've only minimally been looking at the derive, hoping for the builder to get in soon, so that the diff is easier to browse. If this PR is limited to only the last commit (unsure if it is), that would make it easy.

I wish Github did a better job of handling stacked PRs.

Copy link
Member

@pksunkara pksunkara left a comment

Choose a reason for hiding this comment

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

I don't see test cases in derive for hidden and name and about. Let's rebase this PR once the other is merged

@ModProg
Copy link
Contributor Author

ModProg commented Sep 19, 2021

This is rebased now, I'll try to look into adding tests later

Copy link
Member

@epage epage left a comment

Choose a reason for hiding this comment

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

There are several open questions on this PR, including one that re-evaluates an already existing API. At least for me, I can see us resoling some of these in this PR but splitting some others out into clap3 Issues so the remaining design work does not block getting this in, allowing you to use it, and allowing us to start collecting feedback.

I'm also good with owning that remaining design work if you are limited on time. I don't want these details to stall out what will be a great feature!

clap_derive/src/attrs.rs Outdated Show resolved Hide resolved
clap_derive/src/attrs.rs Outdated Show resolved Hide resolved
clap_derive/src/derives/arg_enum.rs Outdated Show resolved Hide resolved
let lit = lits
.iter()
.map(|(name, _, attrs)| {
let fields = attrs.field_methods_filtered(&["alias"]);
Copy link
Member

Choose a reason for hiding this comment

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

There is an inconsistency between Arg and ArgValue that I would consider a bug. Arg::alias is hidden by default and requires Arg::visible_alias. Before, we only had visible alias for ArgValue but with this, we can now handle hidden aliases and so alias should be hidden. We can add support for visible_alias but it doesn't need to be in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

makes sense, and then that would mean we probably should move it to ArgValue. (it would be possible without, but with the consistency point as well makes more sense to change it.)

I'll probably have a look into it in the next days. Thanks for all the help on these PRs.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for your patience and diligence in following through! Too many times we, as a community, lose valuable contributions because people don't have the time to polish

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll implemnt only alias for now which will be hidden in completions etc.

src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
src/derive.rs Outdated Show resolved Hide resolved
src/derive.rs Outdated Show resolved Hide resolved
Comment on lines 118 to 123
fn from_str(input: &str, case_insensitive: bool) -> ::std::result::Result<Self, String> {
let func = if case_insensitive {
::std::ascii::AsciiExt::eq_ignore_ascii_case
} else {
str::eq
};

match input {
#(val if func(val, #lit) => Ok(Self::#variant),)*
#(val if #lit.matches(val, case_insensitive) => Ok(Self::#variant),)*
e => Err(format!("Invalid variant: {}", e)),
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can and should we make from_str an inherent method? It looks like we could loop over Self::variants() and I don't think it would make much of a difference, performance wise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but the ArgValue has no idea of the enum variant. Or what did you mean?
We could make variants return a Vec<(ArgValue, ArgEnum)>.

Copy link
Member

Choose a reason for hiding this comment

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

You're right. For this to work, we'd need an iterator over the variants that we could then call as_arg on.

On one hand, it adds a function to the trait that is only used for implementing the inherent method. On the other hand, it simplifies the trait for people manually implementing it (not common but possible, I have a PR doing just that)

Overall I can go either way

src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
src/build/arg/arg_value.rs Outdated Show resolved Hide resolved
@ModProg
Copy link
Contributor Author

ModProg commented Sep 27, 2021

I will resolve all the documentation in this PR when the methods are stablelized

src/derive.rs Outdated
/// All possible argument choices, in display order.
const VARIANTS: &'static [&'static str];
/// All possible argument values, in display order.
fn arg_values() -> Vec<ArgValue<'static>>;
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I missed this Vec on my last pass. This is requiring an allocation for possible values. It is only once and not much data is being copied, so its not the worst, but it'd still be nice to avoid.

Can't use [] because a size is needed.

I know &[] is annoying but it seems like it'd give best results and the annoying part is limited to one piece of code we generate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can only be done through a constant again, otherwise we are returning temporarry values.
But alias relies on a Vec which means the Iterator cannot be coppied to avoid implementing From<&ArgValue>.
And also not const:

error[E0658]: mutable references are not allowed in constant functions
   --> src/build/arg/arg_value.rs:149:9
    |
149 |         self.aliases.push(name);
    |         ^^^^^^^^^^^^
    |
    = note: see issue #57349 <https://github.com/rust-lang/rust/issues/57349> for more information

error[E0015]: calls in constant functions are limited to constant functions, tuple structs and tuple variants
   --> src/build/arg/arg_value.rs:149:9
    |
149 |         self.aliases.push(name);
    |         ^^^^^^^^^^^^^^^^^^^^^^^

error[E0493]: destructors cannot be evaluated at compile-time
   --> src/build/arg/arg_value.rs:148:24
    |
148 |     pub const fn alias(mut self, name: &'help str) -> Self {
    |                        ^^^^^^^^ constant functions cannot evaluate destructors
...
151 |     }
    |     - value is dropped here
    ```

Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe there is a way I am not seeing, I would also like to avoid Vec here.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the reminder about alias.

The only workaround I can come up with is if we instead provide the value variants (function or constant). The user still pays for the cost for aliases, if they use them, but no other allocations would be made. This also opens us up to providing an inherent method for from_str.

What are your thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not understanding what your proposing.
could you write down the signatures you are proposing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will have problems with & vs no &:

error[E0308]: mismatched types
   --> src/derive.rs:290:9
    |
284 |  / pub trait ArgEnum: Sized + 'static {
285 |  |     /// All possible argument values, in display order.
286 |  |     fn value_variants() -> &'static [Self];
287 |  |
288 |  |     /// Parse an argument into `Self`.
289 |  |     fn from_str(input: &str, case_insensitive: bool) -> Result<Self, String> {
    |  |                                                         -------------------- expected `std::result::Result<Self, String>` because of return type
290 | /|         Self::value_variants()
291 | ||             .into_iter()
292 | ||             .find(|v| v.arg_value().unwrap().matches(input, case_insensitive))
293 | ||             .ok_or_else(|| format!("Invalid variant: {}", input))
    | ||_________________________________________________________________^ expected type parameter `Self`, found `&Self`
...    |
299 |  |     fn arg_value(&self) -> Option<ArgValue<'static>>;
300 |  | }
    |  |_- this type parameter
    |
    = note: expected enum `std::result::Result<Self, _>`
               found enum `std::result::Result<&Self, _>`

For more information about this error, try `rustc --explain E0308`.

We either need to call cloned/copied or return Result<&Self>

Copy link
Member

Choose a reason for hiding this comment

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

If we do this, I lean towards Result<&Self>. Its ok for the proc macro to assume copy/clone and it is probably fine for the trait to make Copy or Clone a super trait, but I'm concerned with what people might put in skipped variants. We'd want to at most do Clone and not Copy so people can have something like a String in it. Returning &Self though gives us full flexibility at the cost of ergonomics.

So the question is ergonomics (Result<Self>) is worth the cost of flexibility (requiring Clone).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel like I'm running against a brick wall with this trait definition and lifetimes etc. maybe I'll come back on your ofter to speak on Discord

Copy link
Member

Choose a reason for hiding this comment

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

I'd recommend getting this into a working state, even if not ideal, and we merge it as-is. We can then play with it and meet up on platform-of-choice and discuss trade offs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright I have pushed a solution using a slice but requiring Clone.

@ModProg
Copy link
Contributor Author

ModProg commented Sep 29, 2021

If there are no changes required for the new ArgValue methods, I'd write documentation for them now.

Concerning the ArgEnum trait, I'm not sure if requiring Clone is the best solution, but returning a Reference is probably even more nuisance to the user as they can then only have structs containing references to ArgEnums.

The only solution here would be to go back to deriving from_str using, we can maybe use a lit here omitting hidden and about:

#(val if #lit.matches(val, case_insensitive) => Ok(Self::#variant),)*

@epage
Copy link
Member

epage commented Sep 29, 2021

Looks great! Go ahead and fill out the docs.

Copy link
Member

@epage epage left a comment

Choose a reason for hiding this comment

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

Thanks for putting in all of this effort to make this top notch!

@epage epage requested a review from pksunkara September 29, 2021 19:21
@ModProg
Copy link
Contributor Author

ModProg commented Sep 29, 2021

Thanks for putting in all of this effort to make this top notch!

Thanks, the only thing I'm not the biggest fan of is the Clone requirenment. I hope we can find something there to lift that.

Copy link
Member

@pksunkara pksunkara left a comment

Choose a reason for hiding this comment

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

Hmm.. I think we can fix it such that we don't need Clone. Unless I am missing something fundamental? Don't worry about doing that now though since you have had issues getting that to work. We can always do it later.

Just a few changes needed.

.try_get_matches_from(vec!["pv", "--option", "123"]);

assert!(m.is_ok());
assert!(m.unwrap().value_of("option").unwrap().eq("123"));
Copy link
Member

Choose a reason for hiding this comment

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

Not sure why this and the below tests are needed. Is it for documenting that the aliases wouldn't change the given value?

fn from_str(_input: &str, _case_insensitive: bool) -> Result<Self, String> {
unimplemented!()
}
fn as_arg(&self) -> Option<&'static str> {
fn arg_value<'a>(&self) -> Option<clap::ArgValue<'a>>{
Copy link
Member

Choose a reason for hiding this comment

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

WDYT about naming this as_arg_value?

Copy link
Member

@epage epage Sep 30, 2021

Choose a reason for hiding this comment

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

The call might allocate (for aliases) and so as_ wouldn't work, though to_ would. I can go either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed it to to_arg_value now

fn from_str(input: &str, case_insensitive: bool) -> Result<Self, String> {
Self::value_variants()
.iter()
.find(|v| v.arg_value().unwrap().matches(input, case_insensitive))
Copy link
Member

Choose a reason for hiding this comment

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

Bad unwrap? Should be handled correctly

Copy link
Member

@epage epage Sep 30, 2021

Choose a reason for hiding this comment

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

The unwrap is correct. value_variants should only return variants that have an associated ArgValue, so arg_value will always return Some(_) in this call.

The .unwrap() could be replaced by an except(). I can go either way; my general rule of thumb has been how close and obvious are the two sides of the contract. The less obvious, the more clear you need to be. I would have assumed this would have been obvious but I just got proven wrong.

let from_str = gen_from_str(&lits);
let as_arg = gen_as_arg(&lits);
let arg_values = gen_arg_values(&lits);
let arg_value = gen_arg_value(&lits);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let arg_value = gen_arg_value(&lits);
let as_arg_value = gen_as_arg_value(&lits);

Once the trait method is renamed

const VARIANTS: &'static [&'static str];
pub trait ArgEnum: Sized + Clone {
/// All possible argument values, in display order.
fn value_variants<'a>() -> &'a [Self];
Copy link
Member

Choose a reason for hiding this comment

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

@epage With the recent const improvements, do you think we change the return to be [Self; T] where T is the number of variants?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think so, const generics are very limited.

I gave it a try on the playground and I didn't see a way to make it work.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75afb67dc29136f6341e6ea4f044fea9

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could actually be possible, I tried that to replace the variants slice befor, the issue I had, was I didn't find all the places where I needed to specify the generic. But I can try again, maybe you get more out of the error messages then me.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but that severally limits how the trait can work because all calls to it have to be generic over the lenth.

Copy link
Member

Choose a reason for hiding this comment

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

I don't see many calls at all. Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this would not solve the Clone requirenment, well maybe in 2021 but not yet if I understand this correctly:

this method call resolves to `<&[T; N] as IntoIterator>::into_iter` (due to backwards
 compatibility), but will resolve to <[T; N] as IntoIterator>::into_iter in Rust 2021.
    `#[warn(array_into_iter)]` on by default
    this changes meaning in Rust 2021
    for more information, see issue #66145 <https://github.com/rust-lang/rust/issues/66145>
    ```

Copy link
Member

Choose a reason for hiding this comment

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

@ModProg, depending on what the code looks like, you can force it to use the correct IntoIterator without the new edition

@pksunkara Yes, there aren't many calls but we have to weigh out the API design for which is less likely to break us. A Clone is not a heavy requirement to put on these types. A per implementation const generic severely limits how the trait can be used.

However, as I said before, I think resolving of this needs to be split out into its own issue and that we should merge this as-is. As we've seen through these discussions, there is a lot of nuance to the implementation for any proposed solution, so rather than continue having a lot of back-and-forth in these PRs, we should merge this as-is rather than block it on incremental improvements and allow someone to focus on both the API design and implementation to reduce back and forths on this.

@ModProg
Copy link
Contributor Author

ModProg commented Sep 30, 2021

The clone requirement can be dropped if instead of using the slice of Self we generate the returned Self value inside the from_str method. So it is a new one from scratch.

@epage epage merged commit 3b2e3ff into clap-rs:master Oct 1, 2021
@pksunkara
Copy link
Member

Agree but #2762 (comment) and #2762 (comment) hasn't been addressed. @ModProg Can you please make a follow up PR for them?

@epage
Copy link
Member

epage commented Oct 1, 2021

Agree but #2762 (comment) and #2762 (comment) hasn't been addressed. @ModProg Can you please make a follow up PR for them?

Sorry, I had assumed my responses resolved it as you were responding to one my my comments but not the others.

@pksunkara
Copy link
Member

pksunkara commented Oct 1, 2021

Your responses proposed another way of addressing them with to_ and expect which I agreed with and was expecting @ModProg to fix them, which is why I didn't reply in there.

@epage
Copy link
Member

epage commented Oct 1, 2021

In each one, I said both the original and the idea I mentioned were ok.

@epage epage mentioned this pull request Dec 6, 2021
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ArgEnum support hidden attribute Support about for possible_values e.g. ArgEnum
3 participants