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

Enum property with custom int discriminants #775

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

Bogay
Copy link
Contributor

@Bogay Bogay commented Aug 30, 2021

Fix #764

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution! I added a few comments.

Once your version is final, you can run cargo fmt to auto-format everything. 🧹

Comment on lines 145 to 146
let keys: Vec<String> = self.values.keys().cloned().collect();
keys.join(",").into()
Copy link
Member

Choose a reason for hiding this comment

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

It looks like you're not using the values (i.e. associated numbers), are you?

Great how you reduced the code to be much more idiomatic 🙂

Copy link
Contributor Author

@Bogay Bogay Aug 30, 2021

Choose a reason for hiding this comment

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

It looks like you're not using the values (i.e. associated numbers), are you?

I didn't aware of that before, so I read the code about export a property in gdnative, and found this line at godot-headers, it seems that I can't export information more than enum names? If so, maybe I should find a workaround to export enum name and its corresponding value?

https://github.com/godotengine/godot-headers/blob/4bae481fc305639a076a69485bed37679149bd32/nativescript/godot_nativescript.h#L56

edit: I don't know why I can't see the preview...code is here

typedef enum {
	GODOT_PROPERTY_HINT_NONE, ///< no hint provided.
	GODOT_PROPERTY_HINT_RANGE, ///< hint_text = "min,max,step,slider; //slider is optional"
	GODOT_PROPERTY_HINT_EXP_RANGE, ///< hint_text = "min,max,step", exponential edit
	GODOT_PROPERTY_HINT_ENUM, ///< hint_text= "val1,val2,val3,etc" <--- this line

Copy link
Member

Choose a reason for hiding this comment

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

I'll reply to the whole issue, as this affects how we continue.

Comment on lines 125 to 130
impl From<HashMap<String, i32>> for EnumHint {
#[inline]
fn from(values: HashMap<String, i32>) -> Self {
EnumHint { values }
}
}
Copy link
Member

@Bromeon Bromeon Aug 30, 2021

Choose a reason for hiding this comment

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

I would rather add a method in EnumHint, e.g. the suggested with_numbers() taking tuples. It allows to create EnumHint from a literal syntax and is consistent with the existing new() constructor -- there's no From implementation for Vec<String> either.

Construction could look as follows (this can even be added to documentation):

EnumHint::with_numbers(vec![
    ("one".into(), 1),
    ("two".into(), 2),
    ("seven".into(), 7),
])

@@ -116,30 +119,31 @@ where
/// ```
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct EnumHint {
values: Vec<String>,
values: HashMap<String, i32>,
Copy link
Member

Choose a reason for hiding this comment

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

My edit on the post came a bit late, maybe you missed it:

Unless we need properties of a map somewhere, I would keep things simple and stay with Vec.

It's probably better for such little-used things, as Vec is much cheaper (performance- and memory-wise).

@Bromeon Bromeon added c: core Component: core (mod core_types, object, log, init, ...) feature Adds functionality to the library labels Aug 30, 2021
@Bromeon Bromeon linked an issue Aug 30, 2021 that may be closed by this pull request
@Bromeon
Copy link
Member

Bromeon commented Aug 30, 2021

Your discovery means that Godot doesn't use numbers in the hint at all. Which probably makes sense, since it uses enum names (strings) to abstract from these very numbers.

But it also means we cannot implement this PR as it is now -- there's no point having private numeric values inside EnumHint, when we don't use them anywhere.


We should probably take a step back and ask what problem we exactly want to solve. In the issue, we had this GDScript code:

enum Dir { Top = -1, Bottom = 1 }
export(Dir) var dir = Dir.Bottom

but this does three things:

  • declares an enum Dir -- not a real type in GDScript 3, but syntax sugar for mapping named constants to numbers (if named, this is a dictionary)
  • exports a number (print dir to verify that)
  • associate a GODOT_PROPERTY_HINT_ENUM to this export, so the user has a nice drop-down in the editor

In other words, using the enum definition and export syntax, the previous code is equivalent to this:

const Dir: Dictionary = {
    "Top": -1,
    "Bottom": 1,
}

export(int, "Top", "Bottom") var dir = Dir["Bottom"]

By the way, you can verify this easily:

print("Selected value: ", dir)                     # 1
print("Enum: ", Dir)                               # {Bottom:1, Top:-1}
print("Is Dict: ", typeof(Dir) == TYPE_DICTIONARY) # true

In other words, the desired functionality is already today possible with godot-rust, by exporting an int and using a list of strings as enum hint.

What might add value however is if this could be done in a more convenient way -- maybe by allowing to use a Rust enum?

@Bogay
Copy link
Contributor Author

Bogay commented Aug 31, 2021

Wow, that's more complicated than my excepted before. I need more time to learn the usage of hint and export to complete this.

@Bromeon
Copy link
Member

Bromeon commented Aug 31, 2021

Sure, don't feel forced to address this now, I think we both underestimated it 😉

Maybe I can copy the important findings to the actual issue. So maybe before doing more implementation work, let's figure out if there's something to be done at all, and if so, what.

@Bromeon Bromeon added this to the v0.10.1 milestone Nov 1, 2021
@Bromeon Bromeon modified the milestones: v0.10.1, v0.10.2 Jul 16, 2022
@Bogay Bogay force-pushed the custom-enum-int branch 2 times, most recently from 7b8deb9 to ef9fb61 Compare August 5, 2022 16:11
@Bogay Bogay marked this pull request as ready for review August 6, 2022 15:49
@Bogay Bogay changed the title [WIP] Enum property with custom int discriminants Enum property with custom int discriminants Aug 6, 2022
@Bogay
Copy link
Contributor Author

Bogay commented Aug 6, 2022

@Bromeon
I think this PR is ready to review. You can use my test project to quickly verify whether it works in Godot editor.
Just build the lib, and open the project to see the dir property of Move node inside Move.tscn.
圖片
I will rebase these changes into 2 ~ 3 commits after it's ready to merge.

BTW, derive ExportEnum can add editor support to the enum, but it can't be directly access from GDScript side (e.g. call Dir.Up in GDScript). Should I implement it in this PR? (If so, I might need to discuss about the design)

@Bogay Bogay requested a review from Bromeon August 6, 2022 16:48
@Bromeon Bromeon modified the milestones: v0.10.2, v0.11.x Oct 1, 2022
@Bromeon Bromeon mentioned this pull request Oct 3, 2022
@Bromeon Bromeon modified the milestones: v0.11.x, unplanned Oct 18, 2022
@Bogay Bogay marked this pull request as draft October 25, 2022 15:31
Copy link
Contributor

@chitoyuu chitoyuu left a comment

Choose a reason for hiding this comment

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

Reminder that as of #964, there is now a more comprehensive solution for ToVariant and FromVariant of numeric enums, integrated into the ordinary derive macros, with #[variant(enum = "repr")]. Please see if you can fit your work into the existing framework, so that we don't have to maintain two separate code paths for the same thing, one being a subset of another.

Comment on lines 140 to 148
self.values
.iter()
.map(|(key, val)| match val {
Some(val) => format!("{}:{}", key, val),
None => key.clone(),
})
.collect::<Vec<_>>()
.join(",")
.into()
Copy link
Contributor

Choose a reason for hiding this comment

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

The original code is iterative for a reason. While format!, collect and join are indeed convenient and pretty, they perform many allocations under the hood. It's often said that premature optimization is the root of all evil, but that does not mean we need to needlessly slow things down just because.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

use super::*;

#[test]
fn deny_non_unit_variant() -> syn::Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Tests for proc-macros are better modeled as UI tests, as they not only allow us to check if the behavior is correct (with a real compiler, too), but also whether the error messages we show to users are up to par.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 532 to 718
#[proc_macro_derive(ExportEnum)]
pub fn derive_export_enum(input: TokenStream) -> TokenStream {
Copy link
Contributor

@chitoyuu chitoyuu Dec 2, 2022

Choose a reason for hiding this comment

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

I'm not sure if this is the best way we can present this in the API. On one hand, naming the macro Export would be too general when it only really works on primitive enums at the present time. On the other hand, users are very unlikely to think of typing derive(ExportEnum) by themselves when the compiler is telling them that they're missing Export.

I don't have a concrete suggestion, but personally I'm leaning toward using Export as the name, and relying on error messages to point users to documentation when they try to use it on something else like a struct (in which case they probably want a custom resource instead).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, I've rename it to Export.

@Bogay Bogay marked this pull request as ready for review October 11, 2023 14:42
@Bogay
Copy link
Contributor Author

Bogay commented Oct 11, 2023

Sorry for the very late reply. I got some time these days and fixed it. If it's OK to merge, I'll reorg it into a few commits.

Reminder that as of #964, there is now a more comprehensive solution for ToVariant and FromVariant of numeric enums, integrated into the ordinary derive macros, with #[variant(enum = "repr")]. Please see if you can fit your work into the existing framework, so that we don't have to maintain two separate code paths for the same thing, one being a subset of another.

The ToVariant / FromVariant derive is indeed what I need in the export impl. I switched to using them instead of implementing them in the export derive.

FYI, The demo project is also updated: https://github.com/Bogay/export-enum-demo

@Bogay Bogay requested a review from chitoyuu October 11, 2023 15:14
Copy link
Contributor

@chitoyuu chitoyuu 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 all the effort! The general design looks good to me, although there are still a number of minor concerns on the implementation details. See below.

I understand that it has been a long journey by now, so if you don't see yourself working any further on this PR, feel free to let me know if you'd like me to carry out any of the changes on your behalf.

bors try

gdnative-core/src/export/property/hint.rs Outdated Show resolved Hide resolved
gdnative-core/src/export/property/hint.rs Outdated Show resolved Hide resolved
gdnative-derive/src/export.rs Show resolved Hide resolved
gdnative-derive/src/export.rs Outdated Show resolved Hide resolved
test/src/test_export_enum.rs Outdated Show resolved Hide resolved
bors bot added a commit that referenced this pull request Oct 12, 2023
@bors
Copy link
Contributor

bors bot commented Oct 12, 2023

try

Build succeeded!

The publicly hosted instance of bors-ng is deprecated and will go away soon.

If you want to self-host your own instance, instructions are here.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

Copy link
Contributor

@chitoyuu chitoyuu left a comment

Choose a reason for hiding this comment

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

A few further things. Now all that's left should be the attribute trigger discussed in the open thread above.

bors try

gdnative-core/src/export/property/hint.rs Outdated Show resolved Hide resolved
gdnative-core/src/export/property/hint.rs Outdated Show resolved Hide resolved
bors bot added a commit that referenced this pull request Oct 14, 2023
@bors
Copy link
Contributor

bors bot commented Oct 14, 2023

try

Build succeeded!

The publicly hosted instance of bors-ng is deprecated and will go away soon.

If you want to self-host your own instance, instructions are here.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

Copy link
Contributor Author

@Bogay Bogay left a comment

Choose a reason for hiding this comment

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

This PR should be almost done. If the code is OK, I would update the docs.

gdnative-derive/src/export.rs Show resolved Hide resolved
Copy link
Contributor

@chitoyuu chitoyuu left a comment

Choose a reason for hiding this comment

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

Looks good to me now. The added UI tests are nice too! Please do whatever else you meant to do and squash the remaining commits. It should be good to merge after that.

Thanks for following through this entire time!

bors try

bors bot added a commit that referenced this pull request Oct 17, 2023
@bors
Copy link
Contributor

bors bot commented Oct 17, 2023

try

Build succeeded!

The publicly hosted instance of bors-ng is deprecated and will go away soon.

If you want to self-host your own instance, instructions are here.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

@Bogay
Copy link
Contributor Author

Bogay commented Oct 17, 2023

Looks good to me now. The added UI tests are nice too! Please do whatever else you meant to do and squash the remaining commits. It should be good to merge after that.

Thanks for following through this entire time!

bors try

@chitoyuu Sorry, I found that if EnumHint is used for string hint, it shouldn't have explicit mapping, or the conversion might be broken. While that's a misuse, StringHint::Enum with mapping is a valid object in Rust. Perhaps we should implement two types to support these two scenarios? I haven't tested for StringHint::File and StringHint::GlobalFile, but my guess is that they should be similar to StringHint::Enum.

@chitoyuu
Copy link
Contributor

chitoyuu commented Oct 17, 2023

Interesting. I think there are two options here:

  1. Have ClassBuilder complain about it when a user tries to register a property with StringHint and explicit mappings. The function can strip (ignore) the values, ignore the whole property (leaving library somewhat usable but likely not behaving as expected), or just panic during init (rendering the resulting library completely unusable).
  2. Revert EnumHint and introduce a new type (EnumEntryint?) for just the IntHint version, as a new variant.

I think option 1 with the soft warning would be the most friendly to users and developers, even though it doesn't adhere strictly to the correctness-by-construction principle. I think this is a case where the input is "correct enough" to be acceptable, and thus not worth the increased complexity and duplicated code paths to reject at compile time.

Which one of the options do you think is best, or maybe something else?

@Bogay
Copy link
Contributor Author

Bogay commented Oct 18, 2023

Which one of the options do you think is best, or maybe something else?

Currently I prefer to warn the user when explicit mappings are given to StringHint, but I am not sure where the check should be inserted. The only option I see is StringHint::export_info when extracting the hint string.

let hint_string = match self {
SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => e.to_godot_hint_string(),
SH::Placeholder { placeholder } => placeholder.into(),
_ => GodotString::new(),
};

Maybe something like this? But that looks like a hack...

        let hint_string = match self {
            SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
                if e.has_value() {
                    godot_warn!("StringHint should not be used with EnumHint with mappings");
                }
                e.to_godot_hint_string()
            }
            SH::Placeholder { placeholder } => placeholder.into(),
            _ => GodotString::new(),
        };

Other options (strip mappings, ignore property, or separate types) all seem to require some additional API changes and more complexity. If we allow breaking changes, I would prefer to introduce a separate type for enum hint with mapping. However, I think it is better to maintain backwards compatibility in this case.

@chitoyuu
Copy link
Contributor

chitoyuu commented Oct 18, 2023

I suppose StringHint::export_info isn't the best place to emit a warning from, but it should be acceptable. Not pretty, but acceptable. You can frame it as sanitation during conversion to make the idea more palatable if you want, I guess. The function does take self by ownership so I think you should be able to implement value stripping without making a breaking change.

If we allow breaking changes, I would prefer to introduce a separate type for enum hint with mapping.

That shouldn't be a more breaking a change than what's already in tree since all the public enums now have #[non_exhaustive] attached to them:

enum IntHint {
    Enum(EnumHint), // old/current type without value
    EnumBikeshed(EnumBikeshedHint), // new type with EnumHintEntry
    // ...
}

enum StringHint {
    // Keep as is
}

However, I think it is better to maintain backwards compatibility in this case.

I agree.

@Bogay
Copy link
Contributor Author

Bogay commented Oct 18, 2023

I suppose StringHint::export_info isn't the best place to emit a warning from, but it should be acceptable. Not pretty, but acceptable. You can frame it as sanitation during conversion to make the idea more palatable if you want, I guess.

Do you have a suggestion? I'm sorry that I don't know how to move this check to somewhere like PropertyBuilder::done (which might be more natural?), because it only gets the Hint object, which I can't access the EnumHint that may be in it.

The function does take self by ownership so I think you should be able to implement value stripping without making a breaking change.

Do you mean something like this?

        let hint_string = match self {
            SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
                if e.has_value() {
                    godot_warn!("StringHint should not be used with EnumHint with mappings");
                }
                // strip all values stored inside `EnumHint`
                e.strip();
                e.to_godot_hint_string()
            }
            SH::Placeholder { placeholder } => placeholder.into(),
            _ => GodotString::new(),
        };

I am not sure if it would be better to implement some general purpose APIs in this case. What do you think?

        let hint_string = match self {
            SH::Enum(e) | SH::File(e) | SH::GlobalFile(e) => {
                if e.iter().any(|ent| ent.value.is_some()) {
                    godot_warn!("StringHint should not be used with EnumHint with mappings");
                }

                // strip all values stored inside `EnumHint`
                for ent in e.iter_mut() {
                    ent.value = None;
                }

                e.to_godot_hint_string()
            }
            SH::Placeholder { placeholder } => placeholder.into(),
            _ => GodotString::new(),
        };

@chitoyuu
Copy link
Contributor

chitoyuu commented Oct 19, 2023

Do you have a suggestion? I'm sorry that I don't know how to move this check to somewhere like PropertyBuilder::done (which might be more natural?), because it only gets the Hint object, which I can't access the EnumHint that may be in it.

I guess we can also add a sanitize(&mut self, errors: &mut SanitizeErrors) function to the Hint trait and give it an empty default implementation so it isn't a breaking change for external implementations. SanitizeErrors would be something the implementer can push std::error::Errors to, which internally can simply be a String for now can simply be a type that keeps track of the amount and emits the errors immediately. This would allow for a better separation between the two steps logically, if emitting an error message from export_hint somehow feels sacrosanct.

I am not sure if it would be better to implement some general purpose APIs in this case. What do you think?

I think the second example is better. I don't sense any use cases for the functions outside this one case. If they ever become necessary we can always extract the code into functions later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Component: core (mod core_types, object, log, init, ...) feature Adds functionality to the library status: postponed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Export enum property with custom int discriminants
4 participants