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

Add user annotations #102516

Closed

Conversation

chocola-mint
Copy link
Contributor

@chocola-mint chocola-mint commented Feb 7, 2025

(Update 2025/2/14) This PR has been superceded by a different solution that doesn't actually involve the concept of "custom annotations," and so it has been closed. The author of this PR has chosen not to work on that solution, but you can find the specifications here if you are interested: #102516 (comment)

Original Post

Closes godotengine/godot-proposals#1316

This PR implements user annotations AKA custom annotations, a feature that allows users to define their own annotations with GDScript, for GDScript.

Interacting with user annotation-based custom editor properties:

godot-annotation-2.0-demo-2.mp4

Editor autocomplete for user annotations:

godot-annotation-2.0-demo-3.mp4

User-implemented compile-time validation for user annotations:

godot-annotation-2.0-demo-4.mp4

You can try out this PR with the project provided here: https://github.com/chocola-mint/GodotDevSandbox/tree/custom-annotations , which demonstrates how user annotations can be used in conjunction with EditorInspectorPlugins to achieve the results above.

Note that the examples are meant to demonstrate usage and capabilities. The user annotation @@ToolButton included here is -mostly- redundant with @export_tool_button existing, for example. (Though it does allow you to embed arguments to be bound to the function as a CSV string, and can even check if the string's valid in compile time!)


Specifications

  • User annotations are created by extending a non-abstract subclass of GDScriptAnnotation, with either GDScript or core C++.
    • The builtin virtual classes GDScriptVariableAnnotation, GDScriptFunctionAnnotation, GDScriptSignalAnnotation, GDScriptClassAnnotation can be extended to add annotations that can only target variables, functions, signals, and classes respectively.
    • This is also why the implementation here doesn't come with some kind of annotation <class name> syntax sugar. (as suggested in the proposal thread)
  • User annotations are defined as metadata associated with some "target" of a GDScript. Currently, class members and classes can all be valid targets, but not expressions, suites (indent blocks), etc.
  • User annotations must implement a constructor (_init), which defines the annotation's parameters, and cannot be a C++ virtual class.
  • User annotations can set the property error_message to a non-empty string inside any callback (virtual function) called by the parser. The parser will then format the error message into a parser error message.
    • This means that, for example, _init can be used to validate the annotation's arguments as well.
  • The builtin GDScript<type>Annotation classes come with _analyze virtual functions that allow the user to read the target's info and execute logic (including validation) based on it.
    • For example, one could require a variable targeted by an annotation to be of type Vector2.
    • Or one could require a function targeted by an annotation to have an exact signature.
  • The virtual function _get_allow_multiple can be overridden to specify if there can be multiple of an annotation. The default is to disallow multiple annotations.
  • User annotations extending GDScriptVariableAnnotation can additionally override _is_export_annotation to act as an export annotation similar to @export_custom. This is mainly for ergonomics, as otherwise for "property drawer" use cases (reasonably common IMO), users would have to write an additional @export after their custom annotation every time they want to use it.
    • Implementing the _get_property_hint, _get_property_hint_string, _get_property_usage virtual functions allows the user to specify the PropertyInfo parameters. If not implemented, the default behaviour is exactly the same as @export.
  • User annotations can be used with the @@ googly eyes prefix. @@<user annotation class name>
    • This disambiguates user annotations from the builtin annotations and allow builtin annotations to retain their original purpose of adding keywords without causing name collisions.
    • Appending parentheses after the class name allows the user to specify arguments for the annotation, which is then used to call the annotation's constructor.
    • If there are zero parameters, the parentheses can be omitted.
    • C++ virtual/abstract classes inheriting from GDScriptAnnotation cannot be used as a user annotation.
  • GDScript.get_members_with_annotations, GDScript.get_member_annotations, and GDScript.get_class_annotations can be used to query user annotations from a GDScript resource.

Some notes

  • User annotations are a GDScript-specific concept and is only implemented for GDScript.
    • Other languages already have their own annotation-like systems and designing interop over this would be quite a mess. (Not to mention how CSharpScript::get_public_annotations isn't even actually implemented at the moment)
  • Design is based off C#'s attributes, but with some notable changes:
    • C# has the user directly extend the System.Attribute class, and use another built-in attribute System.AttributeUsage to specify if the attribute should allow multiples, and what it is allowed to target.
    • Adding a builtin annotation just to serve this one special case (GDScript-extended subclasses of GDScriptAnnotation) is not exactly a good idea in my opinion, and would result in an uglier implementation on the parser/analyzer's end.
    • Instead, these are both implemented as virtual functions, but only C++ GDScriptAnnotations can override the get_target_mask virtual function.
    • The reasoning behind this is so the _analyze virtual function overridden by the user can have a strongly-typed interface. (instead of having to remember how to unpack a Dictionary based on the target type)
      • Ideally this should be a single struct, but unfortunately GDScript doesn't support structs (yet) so we'll have to live with the monstrous 9-parameter GDScriptFunctionAnnotation._analyze.
  • In order to support the @@ syntax, digraph (two-character) prefix support has been added to CodeEdit (which previously only supported single-character prefixes)
  • Is this enough to replace the weird editor-focused export annotations like @export_navigation_flags_2d and @export_range? No, as those add metadata shared across languages (PropertyInfo) and that's why both GDScript's export annotations and C#'s attributes can cause the Godot inspector to show the same UI.
    • I personally believe that these "property drawers" (custom editor controls) should be implemented per-language, and the foundation provided in this PR could allow a GDScript-specific editor implementation to come true, but that would be way out of the scope of this PR.

(Draft PR for now, usability/design feedback highly appreciated)

@chocola-mint chocola-mint force-pushed the custom-annotations branch 8 times, most recently from 36b0cb2 to 715f4d8 Compare February 7, 2025 09:36
@AThousandShips AThousandShips added this to the 4.x milestone Feb 7, 2025
@chocola-mint chocola-mint force-pushed the custom-annotations branch 8 times, most recently from 47573ea to 644248f Compare February 7, 2025 11:27
@AThousandShips
Copy link
Member

AThousandShips commented Feb 7, 2025

Please try to avoid pushing multiple times in a shorty time (17 times in 2 hours) it takes up a lot of valuable runner time

If you need to test things and can't test them by compiling on your own machine consider making a second branch with these changes and run CI on your own fork

@TokageItLab
Copy link
Member

TokageItLab commented Feb 7, 2025

My concern with this PR approach is that Annotation is loosely coupled with core functionality. In other words, I worry about conflicts with the core's function with property_hint/usage.

For example, this implementation looks like it could have additional annotations for properties with range defined by built-in or @export_range, but then which of those annotations should finally take precedence over the validator of the property?

In my opinion, what the core should do in the direction of what is proposed in godotengine/godot-proposals#1316 is to improve the property_hint/usage to make it more user-accessible.

For example, the current property hint “0,1,0.1,or_greater” can only be obtained as a string with dictionary from script, but if we can improve them, it could be parsed into a form that scripts can use immediately, such as min: 0, max: INFINITY, and a static function could be exposed that could be obtained from a class.


Also, such things as associating one property with another is something that is already available to every users.

From my experience with other people's code, there are several ways to do this, such as directly rewriting other properties with “setter” or “_validate_property()”, or doing it in a lazy process with a “dirty flag”, so there is no single way to do it. However, what is consistent here is that it is handled within a single object class and is not delegated to an external class.

Considering this, it should be handled in some virtual function by adding a hint like “relation=(prop)” to property_hint, or by allowing a Callable object as an additional argument when adding a property, in short, it should be handled in a way that extends ClassDB::add_ property().


Probably, the loosely coupled annotation added by this PR is not sufficient to ensure the safety of the actual properties, since it cannot retrieve what the core is already doing, as mentioned above. In other words, for true safety, the user would need to know about properties that have some internal validator on the core side, and then transcribe that behavior to the your annotation class like reverse-engineering, which should be redundant.

In conclusion, I think what this PR is trying to provide is a rather add-on-like solution, and I am concerned about conflicts with the core. Rather than adding a new Annotation class, it would be better to improve the property registration and information retrieval, such as adding a variation of @export, or implementing a robust way to retrieve information from gdscript.

In the process, there may be a major change to re-visit setter and _validate_property() in all classes and replace them with macros in order to make consistent the internal validation method, but I remember there have been several cases of that in the past, such as changes to the BIND or math macro. If there is a valid/sufficient reason to improve the property information for publication, it might be acceptable IMO.

@RedMser
Copy link
Contributor

RedMser commented Feb 7, 2025

@TokageItLab Have you looked through the proposal comments? There are some great use-cases mentioned for custom annotations that go beyond just custom @export-alikes, such as registering classes automatically, tagging members and later looping over them, validating custom rules at compile-time, marking fields for networking or save-load purposes with custom metadata, etc.

On the other hand, I agree that the PR is doing a lot of things which haven't been discussed enough yet. For starters, I think it would make sense to scale the PR back a bunch and start smaller.

While a user might enjoy the power of the different sub-classes to override aspects of the members you're attaching an annotation to, it's setting a lot of complex API in stone, the implications of which need to be thought through more.

I personally would start with just exposing GDScriptAnnotation and the three new methods on GDScript class. Later adding more sub-classes is always possible. Thoughts?

@TokageItLab
Copy link
Member

TokageItLab commented Feb 7, 2025

@RedMser I've seen a few comments on that proposal, but in my experience most of them could be handled with _validate_property(). Although I wondered why there is no mention of _validate_property() in the proposal. So, I think it is necessary to first consider cases that cannot be handled by _validate_property() and so on.

If there are only a few cases that cannot be handled by _validate_property(), etc., and if they can be handled by adding or improvement of property_hint/usage, then I don't think an annotation class is necessary. If they are used frequently, then it would just add more variations of @export. Or it could be just some of the syntax sugar of the function which is called in _validation_property().

However, I still completely agree that we cannot easily retrieve those internal hints/usages from the outside, so I think the first thing that needs improvement is not validation by external class with additional annotations but the internal core property registration/retrieving methods. If it is done well, it would allow users to include custom annotations in their hint/usage (or some additional argument like Callable if we add it), and users could retrieve them in a better format to perform their own validation, without annotation classes.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 10, 2025

Sorry for the late reply in advance.


@TokageItLab

For example, this implementation looks like it could have additional annotations for properties with range defined by built-in or @export_range, but then which of those annotations should finally take precedence over the validator of the property?

Assuming you're talking about GDScriptVariableAnnotation. When _is_export_annotation is overriden to return true, it tries to mark the target variable's parser node (GDScriptParser::VariableNode) as exported, and thus is effectively the same as any other export annotation - you can't have multiple export annotations on the same property to begin with. There's no worry of precedence here because it will result in a parser error.

In my opinion, what the core should do in the direction of what is proposed in godotengine/godot-proposals#1316 is to improve the property_hint/usage to make it more user-accessible.

For example, the current property hint “0,1,0.1,or_greater” can only be obtained as a string with dictionary from script, but if we can improve them, it could be parsed into a form that scripts can use immediately, such as min: 0, max: INFINITY, and a static function could be exposed that could be obtained from a class.

I don't think such a feature is really solving the same problem. See below.

Also, such things as associating one property with another is something that is already available to every users.

From my experience with other people's code, there are several ways to do this, such as directly rewriting other properties with “setter” or “_validate_property()”, or doing it in a lazy process with a “dirty flag”, so there is no single way to do it. However, what is consistent here is that it is handled within a single object class and is not delegated to an external class.

Considering this, it should be handled in some virtual function by adding a hint like “relation=(prop)” to property_hint, or by allowing a Callable object as an additional argument when adding a property, in short, it should be handled in a way that extends ClassDB::add_ property().

I assume you're talking about this proposal (which mentioned the proposal for custom annotations): godotengine/godot-proposals#6750

Custom annotations as implemented here (with its specifications) cannot achieve this anyway (there's no code generation here!), and I don't believe that it should, nor do I believe that that's its purpose. The proposal mentioned also ended up with a different solution that does not involve custom annotations of any sort. (Suggesting that setters/getters should have a two-parameter version that takes the property name as the first argument)

Outside of the proposal above, referencing other class members safely in a custom annotation isn't possible with the current implementation anyway, as the _analyze callback only provides info on the annotation's target. Though implementing a second "postprocess" pass on all annotations (having an _on_analyzer_end callback) could theoretically solve this issue. When/if that happens it'll have to be a separate PR, however.

Probably, the loosely coupled annotation added by this PR is not sufficient to ensure the safety of the actual properties, since it cannot retrieve what the core is already doing, as mentioned above. In other words, for true safety, the user would need to know about properties that have some internal validator on the core side, and then transcribe that behavior to the your annotation class like reverse-engineering, which should be redundant.

Custom annotations at its core is just associating user-defined metadata with class members. It does not perform runtime validation. The only validation that's happening here is with the custom annotation itself - is it receiving the correct arguments, is it being used on a valid target, etc.

Whether custom annotations should be able to modify their targets or not was discussed in the original proposal for custom annotations. I lean towards the idea that custom annotations should not be able to do so on by default, and only allow a limited case-by-case form of modifications on the grounds of pragmatism. This is why I let GDScriptVariableAnnotation optionally be able to act as @export_custom. (modifying the target property by setting its property hint and usage flags)

In conclusion, I think what this PR is trying to provide is a rather add-on-like solution, and I am concerned about conflicts with the core. Rather than adding a new Annotation class, it would be better to improve the property registration and information retrieval, such as adding a variation of @export, or implementing a robust way to retrieve information from gdscript.

The motivation behind custom annotations (as laid out in the original proposal) had nothing to do with extracting data from
@export (or PropertyInfo by extension). The OP wanted custom annotations as a way to uniformally tag which properties to serialize, so that the OP's serializer code can just look for properties with that custom annotation and run custom serialization code according to its type. The OP would not want to use @export here as that would mean running their custom serialization alongside Godot (meaning redundant serialized data), not to mention if the property in question is even compatible with @export in the first place. (it could be a RefCounted type)

There were awkward workarounds mentioned in the thread, such as dictating special prefix combinations to encode how the variable should be serialized:
image

Custom annotations would do away with the need of such a hack, by encoding that information in a type-safe annotation that can validate itself.

For the sake of making the argument more complete, there is this one counterproposal laid out in the reply here: godotengine/godot-proposals#1316 (comment)

In this reply, dalexeev suggested that a builtin annotation like @data(name : String, meta : Dictionary) would be enough to satisfy the need of associating metadata with members. (Plus some syntax for declaring what each name data annotation can target, e.g., annotation @data_serialize(name: String) {targets=var,func})

Compared to a typed custom annotation approach like the one implemented here, this builtin annotation solution has almost zero compile-time validation possible (aside from being able to require the target to be a property, etc.) and requires users to remember the schema of the Dictionary used with a data annotation with the name name every time they want to use it. Needless to say, this is much more fragile compared to the implementation shown here.


@RedMser

While a user might enjoy the power of the different sub-classes to override aspects of the members you're attaching an annotation to, it's setting a lot of complex API in stone, the implications of which need to be thought through more.

As it stands only GDScriptVariableAnnotation has the capability to do so. And only in a very limited form, (only allowing the custom annotation to optionally function as @export_custom) implemented to address a very common use case. (reusable, EditorInspectorPlugin-based custom property editors, which would otherwise require the user to add an additional @export after their custom annotation every time they want to use it)

Everything else is read-only access to GDScript's member info, via target-specific _analyze callbacks for each possible target.

I personally would start with just exposing GDScriptAnnotation and the three new methods on GDScript class. Later adding more sub-classes is always possible. Thoughts?

This means we have to hard-code the target restriction first, for example only allowing variables to be targeted at first and then unlocking that limitation later on in a future version. And this means that custom annotations in its first version would have no way to validate itself based on its target. (can't restrict the target's type, etc.) The first part is acceptable, but the second part would make custom annotations much less safe to use, and I think that's really unfortunate.

Alternatively, as a compromise: Only GDScriptAnnotation (necessary as abstract base class) and GDScriptVariableAnnotation are exposed at first, and the other subclasses (GDScriptFunctionAnnotation, GDScriptClassAnnotation etc.) can be added in later in future updates. (or discussed as separate feature proposals)


@TokageItLab

I've seen a few comments on that proposal, but in my experience most of them could be handled with _validate_property(). Although I wondered why there is no mention of _validate_property() in the proposal. So, I think it is necessary to first consider cases that cannot be handled by _validate_property() and so on.

I think it'd be helpful if you could demonstrate how _validate_property() could be used to address the use cases mentioned in the proposal thread, in a scalable way.

If there are only a few cases that cannot be handled by _validate_property(), etc., and if they can be handled by adding or improvement of property_hint/usage, then I don't think an annotation class is necessary. If they are used frequently, then it would just add more variations of @export. Or it could be just some of the syntax sugar of the function which is called in _validation_property().

Again, custom annotations are not meant to validate a property. It does not reinforce some kind of invariant on the target property on its own. It cannot run any code to say, clamp a float property within a given range. The examples shown here "enforce" that invariant by adding custom editor interfaces where the only possible operations will always result in the property satisfying the invariant. (For example, ExportDirection2D's editor interface, ExportDirection2DEditor, takes the user's mouse position, computes a normalized vector to that point, and saves it as the property's new value) By having a custom annotation here, we've saved on countless LOC that would've been wasted on writing per-class EditorInspectorPlugins - I can add an ExportDirection2D custom annotation to any Vector2 property on any class in the project and ExportDirection2DEditor will figure out that I want an editor UI for editing directions (unit vectors) for that property.

To reiterate, by itself, custom annotations do not perform any validation on the property. A custom annotation only validates itself. (user-defined code to check of the annotation is initialized with proper arguments, and if it is being used on a valid target based on its info via the _analyze callback)

@TokageItLab
Copy link
Member

TokageItLab commented Feb 10, 2025

For example, _validate_property() can be used to change the type according to a certain property, and numerical associations can be done with set() as shown below.

validation.mp4
@tool
class_name ValidationDemo1
extends Node


@export var is_int: bool = false:
	set(value):
		is_int = value
		notify_property_list_changed()


@export_range(0, 100, 0.01) var number: float = 0:
	set(value):
		number = value
		number_multiplied = number * 2


@export_range(0, 200, 0.01) var number_multiplied: float = 0:
	set(value):
		number_multiplied = value
		number = number_multiplied * 0.5


func _validate_property(property: Dictionary) -> void:
	if str(property.name).begins_with("number") && is_int:
		property.type = TYPE_INT
		if property.name == "number_multiplied":
			property.hint_string = "0,200,1"
		else:
			property.hint_string = "0,100,1"

validation.zip

Since these are already widely used methods inside the core, they may override and collapse these behaviors if the Annotation class is loosely coupled. Or Annotation class behavior will be overwritten by core behavior and become meaningless.

Property information is managed in the core by a struct called PropertyInfo, but these are not well described in the documentation, and I agree that it is an advanced use case. So my suggestion is to make it more user-friendly and to manage properties in tightly coupled with the core.

Indeed, there is no way in the core to pre-validate externally set values, but if we want to add such a method, it should add a method such as set_with_validation() to the core and reference the internal PropertyInfo, and shouldn't do it in an external Annotation class.

Also I don't see much point in annotations that are not closely tied to the internal PropertyInfo. By making PropertyInfo user-friendly, annotation conflicts can be known in advance if the user is able to obtain numerical tolerances from the PropertyInfo information in advance; This means that unintended collapse can be avoided to some extent.

For example, if the external annotation class has a float but that is internally set to Int, it means that the float is cast to Int when the user does not intend it to be. In this case, the user would need to know in advance from PropertyInfo that it will be treated as an Int in the core, and set the annotation to Int. The means to know this PropertyInfo should be what is missing from the current core; Since a dictionary of annotations not closely tied to the internal PropertyInfo can be had externally, I believe it would suffice to have it as an add-on. If your complaint is that you cannot define the @ syntax in an add-on, then you should send a proposal suggesting that it be defined in an add-on.


By having a custom annotation here, we've saved on countless LOC that would've been wasted on writing per-class EditorInspectorPlugins - I can add an ExportDirection2D custom annotation to any Vector2 property on any class in the project and ExportDirection2DEditor will figure out that I want an editor UI for editing directions (unit vectors) for that property.

Also, functions such as adding buttons to the editor is clearly not directly related to the annotation, so it must be a separate PR. You can use the EditorInspectorPlugin, but I understand that it is cumbersome. If you really need it, you should send a new proposal to add buttons to hint and usage. Or you could send a suggestion to add a syntax sugar for EditorInspectorPlugins.


This PR seems to contain a lot of stuff that has nothing to do with the essence of annotation.

Please stand back and sort them out.

I have shown above that it is possible to associate properties with each other using set() and _validate_property(). The adding buttons is not directly related to annotations. So it is a misconception "to add annotations in order to add associations and buttons easily".

What we really need here is something that gets value information from property before instantiating the class.

Property type, precision and range information are already included in the PropertyInfo.

If we want to get the behavior of set() and _validate_property(), the behavior will need to be organized according to some format and stored in PropertyInfo. Having an external dictionary (your annotation class) will not help for this. It might be possible to register them in an external dictionary in a macro, but that would be redundant.

Instead, make users allow to read and write PropertyInfo directly easily, which is the straightforward way to do it. What is currently missing is a way to do it before class instantiation. The main point to consider here is what information is missing if PropertyInfo is to be used as annotations.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 11, 2025

I don't see how the _validate_property() use case shown here has anything to do with the use cases of custom annotations. I've already made the point that custom annotations do not validate properties they target. I am not implementing custom annotations as a feature that can change the property's type (or PropertyInfo in general), and that is certainly not what the custom annotation proposal is about in the first place.

To make things extra clear, here's what custom annotations are:

  • Custom annotations are user-defined typed metadata that can target certain class members of a GDScript.
  • The metadata can be read by user code with the GDScript class's query methods.
  • Custom annotations can validate themselves to ensure that they're being used in the way the creator (the user-developer who designed the custom annotation) intended, using _init and _analyze callbacks.

And here's what custom annotations are not:

  • Custom annotations are not magical preprocessors that can change its target's type.
  • Custom annotations do not perform any validation on its target.
  • Custom annotations do not modify the target's value (or anything for that matter!) with any of its callbacks.
  • Custom annotations do not create "associations" between properties of the same class. In fact, at its current state it simply can't, because it doesn't know what other member the class has at the time of the _analyze callback.

And here's what custom annotations can be used for: (Note the passive form! Custom annotations don't act on their own!)

  • Custom annotations can be used to associate structured (strongly-typed) metadata with class members.
  • The metadata can be read by custom serializer logic (user-implemented code that is not provided by this PR) to implement custom serialization independent of Godot's serialization logic.
  • The metadata can be read by custom EditorInspectorPlugins (again, user-implemented code that is not provided by this PR) as a way to reuse custom editor controls with minimal code duplication.

All of the above are already mentioned as use cases in the custom annotation proposal!


For example, if the external annotation class has a float but that is internally set to Int, it means that the float is cast to Int when the user does not intend it to be. In this case, the user would need to know in advance from PropertyInfo that it will be treated as an Int in the core, and set the annotation to Int. The means to know this PropertyInfo should be what is missing from the current core; Since a dictionary of annotations not closely tied to the internal PropertyInfo can be had externally, I believe it would suffice to have it as an add-on.

First of all, this is not my complaint. I made this PR because there's a proposal with over 100 thumb-ups and extensive discussion that went on for 4 years. (with core maintainers involved) There's a need for this and I'm trying to address that need.

It is the responsibility of the designer of the custom annotation in question to perform the required validation logic to ensure that it is not being used in an unsupported situation.

  • If a custom annotation that's meant for floats is being used on int properties, the _analyze callback of GDScriptVariableAnnotation gives enough information for the user to catch the incorrect usage and emit an error message.

And of course, the custom annotation cannot change how the property is serialized. I've said this before and I'll say it again, custom annotations cannot change the property's type nor do the people who wanted custom annotations in the proposal wanted to change the property's type.

If your complaint is that you cannot define the @ syntax in an add-on, then you should send a proposal suggesting that it be defined in an add-on.

That's what the proposal was about! Custom annotations! There was a proposal, and so here I am, making a PR for that proposal!

I think there's a fundamental misunderstanding happening here.

Also, functions such as adding buttons to the editor is clearly not directly related to the annotation, so it must be a separate PR. You can use the EditorInspectorPlugin, but I understand that it is cumbersome. If you really need it, you should send a new proposal to add buttons to hint and usage. Or you could send a suggestion to add a syntax sugar for EditorInspectorPlugins.

The examples I had provided are just example usages. I am in no way suggesting that Godot should have a core annotation of some sort that adds buttons to editors (although that's already a thing: #96290)

Consider how that PR, among many other proposals linked in the custom annotation proposal, could have been rejected from the Godot core and provided as addons, had custom annotations been a thing. With this PR even I could have made a plugin for tool buttons without modifying engine code. It would've meant not adding even more bloat to PropertyInfo.

The examples are not me saying "I want my cool ToolButton syntax in Godot core." Not "I want my cool unit vector editor in Godot core." None of that. They are there to provide a case that custom annotations, as implemented here, are useful and actually usable. Once again I'll emphasize that the example custom annotations are written in GDScript as an addon, as demonstrated here: https://github.com/chocola-mint/GodotDevSandbox/tree/custom-annotations

This PR seems to contain a lot of stuff that has nothing to do with the essence of annotation.

Please stand back and sort them out.

I implore you to take a second look at what the PR actually contains, in terms of engine code.

It does not add custom editors. Nowhere in the changelist is me extending EditorInspectorPlugin. This PR contains just enough changes to satisfy the custom annotation proposal. If that's not "the essence of annotation," I don't know what is.

I have shown above that it is possible to associate properties with each other using set() and _validate_property(). The adding buttons is not directly related to annotations. So it is a misconception "to add annotations in order to add associations and buttons easily".

The misconception here is you believing that "custom annotations" is a feature meant to associate properties of the same class with each other. I've said this again and again and I'll say it again. That's not what they're for, and that's not what the original proposal was asking for either.


If you're simply against the ergonomic feature of GDScriptVariableAnnotation that allows it to optionally act as an export annotation equivalent to @export_custom, I'm perfectly fine with this PR not having that. It'll mean that the PR gets ever so slightly more annoying to use for the use case of reusable custom property editors, but it's not the end of the world.

That said, please understand that aside from this ergonomic feature, custom annotations do not perform any validation on their targets and do not change anything about their types and whatnot.


I'd love to hear feedback from other contributors as well. Pardon me for the mention, but @dalexeev maybe you could chime in seeing how you were involved in the custom annotation proposal's discussion.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 11, 2025

And here's what custom annotations can be used for: (Note the passive form! Custom annotations don't act on their own!)

Custom annotations can be used to associate structured (strongly-typed) metadata with class members.

Why is the current type annotation / PropertyInfo not sufficient?

The metadata can be read by custom serializer logic (user-implemented code that is not provided by this PR) to implement custom serialization independent of Godot's serialization logic.

Shouldn't a way to dump PropertyInfo correctly be added to the core? Is it not sufficient to add a virtual function like _serialize_property() in a similar way to _validate_property() or _get_property_list()?

The metadata can be read by custom EditorInspectorPlugins (again, user-implemented code that is not provided by this PR) as a way to reuse custom editor controls with minimal code duplication.

It is not the direct reason why annotation classes are required. EditorInspectorPlugin can read PropertyInfo by _parse_property(), why is it not enough to include additional information there?


What is passed in the _analyze() argument of the class you created is what is retrieved from PropertyInfo and MethodInfo in the core generally. My question is that it is not clear why you need a new class to store/retrieve them.

Why not just improve the methods associated with PropertyInfo and MethodInfo in the core? If there is information missing there, why not just add it? In the extreme case, the hint_string in PropertyInfo can contain anything, so if you want to have custom data for a property, you can put it all there. If this is not sufficient, then we need to find the right direction for improvement based on the reasons.

That's what the proposal was about! Custom annotations! There was a proposal, and so here I am, making a PR for that proposal!

No. What I am suggesting is more generic and does not target specific things such as properties or methods. In short, I am proposing to allow the creation of add-ons that use specific symbols as declarations through some sort of register to gdscript_highlighter/parser.

The annotations in this PR are hard-coded for property and method at your discretion. As mentioned above, the core has already established a method to process such information by using PropertyInfo/MethodInfo and set() and _validate_property().

It is redundant to add annotation classes that have the same purpose with them (PropertyInfo/MethodInfo and set() and _validate_property()). For example, it could cause the core code to have two EditorInspectorPlugins, one that uses the annotation class and one that does not, which reduces maintainability.

Therefore, I argue that those annotation classes should not be added, and that this kind of information should be consolidated in PropertyInfo/MethodInfo and focus on improving methods to make them accessible to users.

Well, my conclusion would be similar to what @RedMser says:

I personally would start with just exposing GDScriptAnnotation and the three new methods on GDScript class. Later adding more sub-classes is always possible. Thoughts?

There is only one GDScriptAnnotation that can be considered whether it can be added or not. The other 4 subclasses should never be added (the reason has been fully explained above). However, I still wonder if it is necessary to make new class GDScriptAnnotation. Since gdscript already have the method which retrieve class information like get_method_list(), shouldn't it be possible to get annotation-dumped arrays, etc. from some methods in a similar way-- Wouldn't it be better to manage annotations in ClassDB rather than making modifications to GDScript classes to specialize for annotations?

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

If the annotation does not have a member name as your case C, isn't it not much different from the following?

@export var user_name: String:
	set(value):
		user_name = _validate_length(value)

@export var user_name2: String:
	set(value):
		user_name2 = _validate_length(value)

func _validate_length(str: String, max_length: int = 5) -> String:
	if max_length < 0:
		printerr("Error")
		return ""
	return str.substr(0, max_length)

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

This is why this is not a good use case. As I have reminded you before, custom annotations do not and cannot validate property values. The features introduced by this PR cannot validate property values.

When custom editors are needed, adding a _validate_length function is no longer sufficient.

Though even in this case, custom annotations do have a small advantage in that less keystrokes is required on the user of the custom annotation's side, and illegal values show up as compile errors. But that's besides the point. Custom annotations do not and cannot validate property values.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

If we just want to annotate that the length is 5, case B is almost sufficient.

@export var max_length: int = 5
@export var user_name: String

is that in B, max_length is a per-instance property,

Indeed, there is a problem with instantiation. However, I believe it is related to static member features and shouldn't be solved by adding a class like GDScriptVariableAnnotation.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

Sorry, but I think you're missing the point...

What custom annotations bring to the table is way more than just the difference between static and non-static properties. And of course, static properties are not class metadata. No book on metaprogramming would consider static members of a class to be metadata.

I sound like a broken record at this point so I'll ask you to re-read every use case I've mentioned so far in this thread.

And, once again, custom annotations do not and cannot validate property values. Please understand. If you want them to, we can talk about the possibility of allowing them to do so, but at the moment they just can't.

@TokageItLab
Copy link
Member

And, once again, custom annotations do not and cannot validate property values.

If it does nothing, then I don't see why you need a method like _analyze(). My statement still stands, Annotation is much simpler, it just gives a dictionary (typed ideally) to any target.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

Straight from the OP:
image

It does not do nothing. The demo videos attached even demonstrated this self-validation in action. (third one from the top)

Also see sample use cases of _analyze here:

  1. https://github.com/chocola-mint/GodotDevSandbox/blob/custom-annotations/addons/custom_annotations/ExportDirection2D.gd
  2. https://github.com/chocola-mint/GodotDevSandbox/blob/custom-annotations/addons/custom_annotations/ToolButton.gd

I'm not convinced that you've given the OP a proper read. I beg of you, please read it again. Take your time and try out the sample project with this PR. Otherwise this discussion will go nowhere.

@TokageItLab
Copy link
Member

I wonder about the self-verification in the first place. I think it is due to the fact that AnnotationClass is an object.

Outside of the proposal above, referencing other class members safely in a custom annotation isn't possible with the current implementation anyway, as the _analyze callback only provides info on the annotation's target. Though implementing a second "postprocess" pass on all annotations (having an _on_analyzer_end callback) could theoretically solve this issue. When/if that happens it'll have to be a separate PR, however.

Wouldn't these problems be solved by just getting a list of annotation dictionaries and targets set from ClassDB, and that's it? Then there would be no need for AnnotationClass in the first place.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

From an implementation standpoint, ClassDB only records info about C++ classes (registered with GDREGISTER_CLASS macros) and it would be impossible to use it to record annotations about GDScript classes, which are dynamically registered and unregistered (to ScriptServer I believe) as they are loaded and unloaded, and sometimes don't even have names.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

ClassDB can read and return the gdscript's ClassInfo parsed and stored by gdscript_parser, and the list of properties and methods are also retrieved through it. I assume it is possible to store annotations as metadata in ClassInfo in the form like struct CustomAnotation.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

ClassDB can read and return the gdscript's ClassInfo parsed and stored by gdscript_parser, and the list of properties and methods are also retrieved through it. I assume it is possible to store annotations as metadata in ClassInfo in the form like struct CustomAnotation.

This is just false, as far as I can see. If you don't believe me, open up gdscript.cpp, gdscript_parser.cpp, gdscript_analyzer.cpp, and gdscript_compiler.cpp, use a search tool (or just Ctrl+F in browser) and look for ClassDB::. You will not find any ClassDB::register_class calls. ClassDB does not store GDScript class infos. (And again, it can't. GDScript classes don't even need to have names!)

You might be thinking of how Object::get_property_list under the hood tries to call ScriptInstance::get_property_list if it has a script attached. That does not actually involve ClassDB.

Storing custom annotations in GDScript resources as non-serialized data is as far as I can see the best solution here.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

Oh sorry, that is my mistake. It is true that ScriptInstance is used for gdscript. If that is the case, I think it is possible to get the annotation list from ScriptInstance, isn't it?

I am beginning to understand what my discomfort with AnnotationClass is. Perhaps it is because most of what _analyze() is trying to do should be done in the virtual methods of the object class, not external class (like Annotation classes), so the design doesn't seem to match Godot.

What I have seen in Proposal is in fact such comments godotengine/godot-proposals#1316 (comment) godotengine/godot-proposals#1316 (comment) something that would be implemented as a virtual method of an object class, and I would prefer a simple implementation of iterating the dictionary list, much of Godot things is implemented that way. This does not apply to items that do not need to be listed, but annotations are the ones that should be listed IMO.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

Oh sorry, that is my mistake. It is true that ScriptInstance is used for gdscript. If that is the case, I think it is possible to get the annotation list from ScriptInstance, isn't it?

ScriptInstance is an abstract virtual class, so GDScript implements it with GDScriptInstance, which then implements ScriptInstance::get_property_list.

If annotation objects are stored in ScriptInstance, then CSharpScriptInstances would also have them, which would be very weird. I've mentioned this before in the OP, but CSharpLanguage doesn't even implement CSharpLanguage::get_public_annotations. The concept of an annotation only means something to GDScript, so it makes sense to store them in the GDScript resource class.

I am beginning to understand what my discomfort with AnnotationClass is. Perhaps it is because most of what _analyze() is trying to do should be done in the virtual methods of the object class, not external class (like Annotation classes), so the design doesn't seem to match Godot.

GDScriptAnnotation is an object. It's instantiated and stored in GDScript resources after parsing and analyzing the code inside the GDScript resource. The virtual functions belong to GDScriptAnnotation. (and its C++ subclasses for _analyze)

It's not "Godotic" (I made this word up) because the Object system has so far operated on a per-instance basis.
get_property_list depends on the object's script instance, etc. Annotations are per-class and not per-instance, and that's why you think it's not "Godotic."

Think about it like this.

  • Annotations (the GDScript concept) are per-class.
  • Property hints/hint strings/usage flags are per-instance.
  • Annotations are compiled into default property hints/hint strings/usage flags for a class, but each instance can also override and modify these PropertyInfo, with Object's virtual methods like _get_property_list.
  • Custom annotations should be analogous to annotations (the GDScript concept), and so they are per-class and not per-instance.

It's not "Godotic" in this sense but I think it's very much justified for this.

What I have seen in Proposal is in fact such comments godotengine/godot-proposals#1316 (comment) godotengine/godot-proposals#1316 (comment) something that would be implemented as a virtual method of an object class, and I would prefer a simple implementation of iterating the dictionary list, much of Godot things is implemented that way. This does not apply to items that do not need to be listed, but annotations are the ones that should be listed IMO.

I don't think that is a good solution. Consider the merits of a strongly-typed custom annotation system. Merits which I have already demontrated in the OP:

  • User-defined annotation validation that makes custom annotations very user friendly - incorrect usage is immediately detectable as user-implemented compile errors.
    • For advanced usages, the @@ToolButton example takes a string argument and parses it as comma-separated function arguments, and if anything goes wrong it can immediately report a parsing error to the user on the spot.
  • Implemented as classes, so they can be extended and used incredibly intuitively. You can even use them without creating a plugin, where as that version requires you to call Engine.add_annotation somewhere in the editor.
    • There are other comments trying to add to that solution by proposing new annotation keywords. This increases GDScript parser complexity and I don't think is ideal either.
  • Users are given enough power to have robust validation and argument data processing for their custom annotations, but have basically no access to the GDScript abstract syntax tree. (AST)
    • Compared to that proposal, which would require core modification to expose even more settings if any argument validation is required. In fact, at a glance it seems to be entirely typeless, using variants for arguments and having no way to specify types.
  • This PR's implementation is pretty elegant IMO, for such a major feature. Of course it still needs some polishing here and there, but the changes are very self-contained and don't involve sweeping changes across the core. The vast majority of the changes are in the GDScript module.

What's wrong with iterating over a list of GDScriptAnnotations as opposed to iterating over a list of Dictionarys? You get more type guarantees and don't need to use magic strings for accessing members of a custom annotation.

The implementation proposed in the comment you linked is only "simple" at a glance but is underpowered, lacks the level of UX people expect from a user-definable version of annotations, and requires changes outside the GDScript module.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

Consider the merits of a strongly-typed custom annotation system.

Type safety is a good thing, but even if only your annotation classes are type safe, get_property_list(), _validate_property(), etc. are not. Instead, it is better to align with other implementations.

Then, if get_property_list() and _validate_property() are to be type-safe in the future, then annotation should also be type-safe. If they are similarly implemented, it will be easy to align them and rework them, but if they are disparate implementations, it will be difficult.

Also it is rare, it is possible that there may be changes to structures such as PropertyInfo/MethodInfo, such cases should be assumed.

What's wrong with iterating over a list of GDScriptAnnotations as opposed to iterating over a list of Dictionarys?

If the Annotation class could be referenced, if there are multiple Annotations with the same reference in a single script and they are iterated on, wouldn't this cause unintended duplicate processing if the user is not supposed to that those have same reference?

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

Type safety is a good thing, but even if only your annotation classes are type safe, get_property_list(), _validate_property(), etc. are not. Instead, it is better to align with other implementations.

If get_property_list() and _validate_property() are to be type-safe, then annotation should also be type-safe in the future. Although it is rare, it is also possible that there may be changes to structures such as PropertyInfo/MethodInfo.

As I have explained above, annotations don't exist at the same level as get_property_list and _validate_property. Annotations have always been per-class, it is PropertyInfo/MethodInfo that's per-instance and depends on the Object's virtual method overrides.

Consider how the query method get_member_annotations is not implemented on Object but rather on GDScript. There is no hidden convention broken here. Per-instance PropertyInfo/MethodInfo queries are not type-safe, but per-class annotation queries can still be.

Also it is rare, it is possible that there may be changes to structures such as PropertyInfo/MethodInfo, such cases should be assumed.

Thankfully, GDScriptAnnotation's implementation is entirely independent from PropertyInfo and MethodInfo. The data passed to _analyze are extracted from parser nodes after analysis.

If the Annotation class could be referenced, if there are multiple Annotations with the same reference in a single script and they are iterated on, wouldn't this cause unintended duplicate processing if the user is not supposed to that those have same reference?

Every usage of a custom annotation in a single script creates an independent instance.

@@ExportDirection2D # Instance 1
var direction := Vector2.RIGHT

@@ExportDirection2D # Instance 2
var other_direction := Vector2.RIGHT 

No duplicate processing can happen here, when calling GDScript.get_member_annotations, even if a member has multiple annotations of the same type. They are different objects. Plus, GDScriptAnnotations by default disallow multiple annotations of the same type on the same object, and the user have to override _get_allow_multiple to change that setting.

@TokageItLab
Copy link
Member

Thankfully, GDScriptAnnotation's implementation is entirely independent from PropertyInfo and MethodInfo. The data passed to _analyze are extracted from parser nodes after analysis.

It means that the arguments of _analyze() may change. They may also need to be changed when struct is implemented. This is not a good thing because it breaks compatibility. That is why I recommend using PropertyInfo directly.

@chocola-mint
Copy link
Contributor Author

Thankfully, GDScriptAnnotation's implementation is entirely independent from PropertyInfo and MethodInfo. The data passed to _analyze are extracted from parser nodes after analysis.

It means that the arguments of _analyze() may change. They may also need to be changed when struct is implemented. This is not a good thing because it breaks compatibility. That is why I recommend using PropertyInfo directly.

Would they? Look at the current arguments of GDScriptVariableAnnotation._analyze. Notice how only the variable name (name), the variable type name (type_name), the variable type id (builtin_type, as Variant.Type enum), and whether it's a static variable or not (is_static) are there.

GDScriptVariableAnnotation._analyze actually provides less information than PropertyInfo, and this is by necessity. PropertyInfo is just not available at the stage of GDScript parsing! Same goes for GDScriptFunctionAnnotation._analyze and MethodInfo!

I can't use PropertyInfo or MethodInfo here not just because I shouldn't but also because I can't.

@TokageItLab
Copy link
Member

I can't use PropertyInfo or MethodInfo here not just because I shouldn't but also because I can't.

So I suggest we should first address that issue.

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 12, 2025

I can't use PropertyInfo or MethodInfo here not just because I shouldn't but also because I can't.

So I suggest we should first address that issue.

And I'm trying to tell you that PropertyInfo/MethodInfo is per-instance and we should keep treating it that way. It would also be consistent with the naming here - it's called GDScriptVariableAnnotation because it targets GDScript variables (properties are per-instance), and it's called GDScriptFunctionAnnotation because it targets GDScript functions. (methods are per-instance)

This is a physical limitation of the parser/analyzer/compiler and it is absolutely not worth it to solve this issue to create an unnecessary dependency and get a worse API. Less is more in this case!

Plus, PropertyInfo and MethodInfo don't perfectly match the needs of GDScriptAnnotation either. There's no field indicating whether a "property" is static or not, and there's no field indicating whether a "method" is a coroutine (async method) or not either. Changing PropertyInfo and MethodInfo to answer the needs of GDScriptAnnotation would be a bad idea too as those two structs are designed for engine interop. (both GDScript and C# use these, and not all language concepts can be shared)

@TokageItLab
Copy link
Member

TokageItLab commented Feb 12, 2025

I understand that annotaion should be defined for a class, not an instance.

However, I still believe that the validation should be done by getting the annotation from a validation method (such as _validate_property() or set()) that already exists and is well used on the instance side. At this point, there is only one way to validate a property or method in the core.

Having the validation in the Annotation class makes them two. Even if you insist that Annotation class's validation (_analyze()) is to validate @export_xxx at a class which is lower layer then an instances, it is up to the user to use it; Users can use it as an alternative to _validate_property() or validation in the set().

As I said above, for maintainability reasons we should not provide two methods of validation that can target the same thing.

If we could unify all validation with the Annotation class, I would agree, but I don't think that is something that should be done at this stage of Godot 4.

I can agree that we should reconsider the validation method when Godot 5 comes around. However, while in Godot 4, subclasses with _analyze() should not be added, but instead should just be allowed to retrieve annotations to the instance.

@dalexeev
Copy link
Member

In this reply, dalexeev suggested that a builtin annotation like @data(name : String, meta : Dictionary) would be enough to satisfy the need of associating metadata with members. (Plus some syntax for declaring what each name data annotation can target, e.g., annotation @data_serialize(name: String) {targets=var,func})

I'd love to hear feedback from other contributors as well. Pardon me for the mention, but @dalexeev maybe you could chime in seeing how you were involved in the custom annotation proposal's discussion.

Thank you for your work and dedication to improving GDScript!

In my opinion, custom annotations are a somewhat overrated feature. Many of us think it would be cool to have them, but no one provides clear requirements for what custom annotations should or shouldn't do. What problems would you solve with them? Should annotations simply be metadata about class members (attributes/tags), support a limited set of features (like export variables, function decorators), or should they support all the capabilities of built-in annotations (modifying behavior at the language level)? Perhaps they could support a wide range of metaprogramming elements, something like macros?

Let's assume the simplest scenario, where we limit annotations to metadata about class members, similar to attributes in PHP. I have the following questions regarding the concept and implementation options:

1. Do we really consider this approach important and useful? If desired, users could store metadata like this:

const MEMBERS_INFO = {
    login = {name = "Log In", args = ["user", "password"], returns = "token"},
    logout = {name = "Log Out", args = ["token"], returns = null},
}

func login(args: Array) -> Variant: pass
func logout(args: Array) -> Variant: pass

The downsides are that class members and metadata are separated, there's no compile-time validation, and there's no standardized interface, each project could implement this differently. Custom annotations could become the standard for storing class member metadata.

2. Do we really consider it necessary to declare/register custom annotations? We could allow the use of any annotations (with a prefix like @@, @custom_, or @data_) without any validation, or we could introduce a special annotation (e.g., @data(key: String, value: Variant) or @tag(data: Variant)). It might look something like this:

@data_action("Log In", ["user", "password"], "token")
func login(args: Array) -> Variant: pass

@data_action("Log Out", ["token"])
func logout(args: Array) -> Variant: pass

The downside is the lack of any compile-time validation.

3. If we want to add limited validation (existence of the annotation, allowed targets, number and types of arguments), we don't necessarily need to follow the C# approach. We could allow annotations to be declared like this:

annotation @@action(name: String, args: Array = [], returns: Variant = null) targets (var, func)

@@action("Log In", ["user", "password"], "token")
func login(args: Array) -> Variant: pass

@@action("Log Out", ["token"])
func logout(args: Array) -> Variant: pass

More advanced validation would require advanced compile-time evaluations, which GDScript doesn't currently support, or a tool mode for annotation classes.

4. In both the case of annotation classes and a special syntax for declaring annotations, we need to address the issue of script loading order. Before performing static analysis on any script, GDScript would need to analyze the scripts where used annotations' declarations or their classes are defined. This presents a certain complexity, given how GDScript works. Additionally, as mentioned earlier, I'm skeptical about annotation classes, as they would require executing code in the editor. If we do go down this path, I'm not sure creating multiple instances for the same annotation is a good idea.

5. There's also the question of how to retrieve this metadata about class members. There has already been heated discussion about a potential equivalent of get_property_list() versus new methods in the GDScript class. I lean toward the latter option, though I'll note that (object.get_script() as GDScript) looks inelegant.

Perhaps we shouldn't rush with this feature? Quality-of-life editor improvements (code formatting, renaming), fixes to the type system, static analysis, performance, etc., seem more prioritized and in demand. It would also be good to refactor many of the accumulated issues in the implementation (static analyzer, compiler, caching/loading system). Maybe we should first implement namespaces to avoid introducing the slightly awkward @@ syntax for custom annotations.

See also:

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 13, 2025

@TokageItLab

Having the validation in the Annotation class makes them two. Even if you insist that Annotation class's validation (_analyze()) is to validate @export_xxx at a class which is lower layer then an instances, it is up to the user to use it; Users can use it as an alternative to _validate_property() or validation in the set().

I've said this before, and I'll say it again. Annotations can only validate themselves, and the "validation" here is not the same thing as _validate_property or _set. If the term "validation" is confusing to you, then think of it as "verification" instead.

I've shown you how it's entirely read-only and does entirely different things. (Annotation classes cannot change anything about the class's property infos. They can't add, change, or remove existing property infos. At best it's just outputting an error message) They have fundamentally different purposes and I absolutely don't see how the user can possibly confuse them.

You cannot use _init or _analyze to replace or replicate _validate_property or _set whatsoever. There's no "two ways of doing the same thing" going on here.


@dalexeev

First of all, thank you for your time. I really appreciate it.

In my opinion, custom annotations are a somewhat overrated feature. Many of us think it would be cool to have them, but no one provides clear requirements for what custom annotations should or shouldn't do.

I'll say that it's "opinionated" instead of "overrated." In the proposal thread, there have been several different interpretations of what a custom annotation should be, from people coming from different programming languages, but no conclusion as there's been no concrete implementation so far - only specifications written on paper, basically theorycrafting.

Consider this PR my effort to push the proposal closer to reality by actually offering a possible implementation, one everyone can judge for themselves if it has good usability, and if it actually solves their problems. You too are welcome to build the PR and open up the sample project to examine the pros and cons of this particular interpretation of custom annotations first-hand. 🙂

Should annotations simply be metadata about class members (attributes/tags), support a limited set of features (like export variables, function decorators), or should they support all the capabilities of built-in annotations (modifying behavior at the language level)? Perhaps they could support a wide range of metaprogramming elements, something like macros?

My answer to these questions, with this PR, would be:

  • Custom annotations are first and foremost metadata about class members. This PR does this job very well, by allowing developers of the custom annotations to make sure that the users of the custom annotations are using them correctly, with _init, _analyze, and the error_message property.
  • Custom annotations can be made to support a limited set of features as needed by extending the GDScriptAnnotation subclasses later on. In other words, the Godot core reserves the rights to consider what additional features should be provided in the future, each of which can be discussed as separate proposals.
    • Export variables (in the form of GDScriptVariableAnnotation being able to optionally function as @export_custom) is one that is trivially implementable and I've included it as it's a common use case. But of course, this PR can very well exist without this particular feature.
    • Maybe all of such proposals end up getting rejected in the future. In which case this PR's custom annotations would still function perfectly well without them. It's future-proof.
  • Custom annotations should not support all capabilities of built-in annotations. In the sense that, custom annotations should not be able to freely manipulate GDScript's abstract syntax tree. (parser nodes)
    • My reasoning behind this is that GDScript's abstract syntax tree should be considered an implementation detail and not outward-facing API. By exposing parser nodes, future GDScript parser/analyzer development must be made while maintaining API guarantees. This would add unnecessary maintenance cost and hinder the GDScript module's growth as a whole.
  • Metaprogramming support? Of course not. (At the very least, that would be outside the scope of the proposal and should be an entirely separate proposal)
    • I do have my other thoughts about GDScript metaprogramming as a whole, like how they probably should be implemented via builtin annotation-based "hooks" called during compilation, and maybe custom annotations can be taken advantage of by a hypothetical metaprogramming codegen script, much like how C#'s custom code generator can be used to generate code based on attributes. But of course, all of this is irrelevant to the discussion, as custom annotations do not support codegen. (metaprogramming)
  1. Do we really consider this approach important and useful? If desired, users could store metadata like this:
const MEMBERS_INFO = {
    login = {name = "Log In", args = ["user", "password"], returns = "token"},
    logout = {name = "Log Out", args = ["token"], returns = null},
}

func login(args: Array) -> Variant: pass
func logout(args: Array) -> Variant: pass

The downsides are that class members and metadata are separated, there's no compile-time validation, and there's no standardized interface, each project could implement this differently. Custom annotations could become the standard for storing class member metadata.

You pointed out the downsides very well. Indeed this is where class-based custom annotations excel over per-class "metadata" as const/static properties.

  1. Do we really consider it necessary to declare/register custom annotations? We could allow the use of any annotations (with a prefix like @@, @custom_, or @data_) without any validation, or we could introduce a special annotation (e.g., @data(key: String, value: Variant) or @tag(data: Variant)). It might look something like this:
@data_action("Log In", ["user", "password"], "token")
func login(args: Array) -> Variant: pass

@data_action("Log Out", ["token"])
func logout(args: Array) -> Variant: pass

The downside is the lack of any compile-time validation.

The downside is a big one. For a feature called "custom annotations," it makes sense for the user to expect a level of usability akin to builtin, vanilla annotations, which feature compile-time validation of arguments as well as autocomplete.

The autocomplete issue can be worked around if annotations have to be registered somehow, (like the Engine.add_annotation proposal mentioned by TokageItLab before, or this PR's class-based approach) but validation is only feasible with this PR's approach.

Usability is an important factor IMO. Having type safety, autocomplete, and powerful user-defined validation of custom annotations (not _validate_property()) changes how the average user perceives and treats custom annotations. The bare minimum "associate typeless variants/dictionaries with class members" could technically get the job done but it would be very annoying to use for every user from small game jam devs, solo indie devs, to bigger indie productions with multiple devs, and we would end up with a solution that technically solves a problem but in a way that's no better than third-party hacks. (if they exist)

(Using another issue as an example, a core solution to "conditional compilation" should provide at least similar level of usability to your gdscript-preprocessor. (Not exact implementation, usability) Excellent plugin by the way!)

  1. If we want to add limited validation (existence of the annotation, allowed targets, number and types of arguments), we don't necessarily need to follow the C# approach. We could allow annotations to be declared like this:
annotation @@action(name: String, args: Array = [], returns: Variant = null) targets (var, func)

@@action("Log In", ["user", "password"], "token")
func login(args: Array) -> Variant: pass

@@action("Log Out", ["token"])
func logout(args: Array) -> Variant: pass

I've seen this solution too in the proposal thread. The logistics of having a special annotation keyword for registering annotations has a lot more implications than this PR's approach. (which takes advantage of the existing class system) For starters it'd require a lot more changes to the gdscript parser and analyzer.

The Traits PR uses a new keyword and I think it's justified there - there's no way of implementing traits as a class of course, but for custom annotations I don't think it's necessary. Or rather, I've demonstrated that it's not necessary. The KISS principle applies here I believe.

More advanced validation would require advanced compile-time evaluations, which GDScript doesn't currently support, or a tool mode for annotation classes.

I invite you to see the advanced validation use case demonstrated in the @@ToolButton example. (Complex validation that matches a comma-separated string of arguments with the target method) As well as the @@ClassTag example. (Validation via reading from another resource)

This is already supported here.

  1. In both the case of annotation classes and a special syntax for declaring annotations, we need to address the issue of script loading order. Before performing static analysis on any script, GDScript would need to analyze the scripts where used annotations' declarations or their classes are defined. This presents a certain complexity, given how GDScript works.

Currently, annotation classes are not resolved until the analyzer stage, at which point they are just resolved like other class references. Which is to say, they work exactly like any other class reference.

  • This is to say that at the parser stage, we only know that it's a custom annotation (from the @@ prefix) but not which one it is. This is why target validation (can this annotation be applied to this target?) is delayed until the analyzer stage.

Additionally, as mentioned earlier, I'm skeptical about annotation classes, as they would require executing code in the editor. If we do go down this path, I'm not sure creating multiple instances for the same annotation is a good idea.

While I can understand skepticism in regards to "executing code in the editor," I don't understand why you're against creating multiple instances for the same annotation. If anything this makes them easier to use as they can have properties and work just like any other GDScript class.

I've demonstrated before that you can query a member's annotations and retrieve metadata from each instance, which would correspond to the arguments you used to "initialize" the custom annotation with. It's certainly way more intuitive.

Sure, one could argue that this generates a bit more memory waste compared to an entirely functional programming-based approach to custom annotations, but it's infinitely more understandable. (Could you imagine if "static virtual functions" were introduced here instead of the _init and _analyze virtual functions?) And besides, it's per-class and not per-instance metadata so the memory implications are trivial.

  1. There's also the question of how to retrieve this metadata about class members. There has already been heated discussion about a potential equivalent of get_property_list() versus new methods in the GDScript class. I lean toward the latter option, though I'll note that (object.get_script() as GDScript) looks inelegant.

I think I've made my case already, when it comes to the relationship between custom annotations and PropertyInfo.

Regarding object.get_script() as GDScript:

  • It looks inelegant but I do think it makes perfect sense considering what the user is trying to do here.
    • The as GDScript part: Annotations are a GDScript-specific concept, but an object doesn't necessarily need to be extended using GDScript in Godot. There could be C#-based objects too. The code here reflects the reality that Godot has to support both GDScript and C# at the same time.
    • The object.get_script() part: Annotations have always been per-class and not per-instance. Export annotations are compiled to default per-instance PropertyInfo/MethodInfo, but the annotation themselves are still per-class. That you're writing object.get_script().get_member_annotations("member") instead of object.get_member_annotations("member") reflects this fact - You're getting the per-class custom annotations and not the per-object PropertyInfos/MethodInfos.
    • Side note: Object.get_script() inexplicably returns a Variant instead of an Object. I still don't get why this is the case...
  • When compared to other languages, I think aside from the as GDScript part it's pretty much the same as any other "accessing class metadata" code of any other language. In C# the equivalent would be object.GetType(), etc.

Perhaps we shouldn't rush with this feature? Quality-of-life editor improvements (code formatting, renaming), fixes to the type system, static analysis, performance, etc., seem more prioritized and in demand. It would also be good to refactor many of the accumulated issues in the implementation (static analyzer, compiler, caching/loading system). Maybe we should first implement namespaces to avoid introducing the slightly awkward @@ syntax for custom annotations.

I don't think the implementation here conflicts with any of the other (all really good) proposals you have here. I'm not saying that this feature should say, be added to master for 4.5, (would be cool but I'm in no hurry) but I don't think it should be rejected outright either simply on those grounds.

Regarding namespaces in particular, they won't be enough to solve the @@ syntax. Hypothetically using @<namespace>.<custom annotation> would just mean that the namespace would have name collisions with builtin annotations. If I put my custom annotations in a namespace called serialize, now if a new builtin annotation called @serialize gets added to the core, that will be an unfortunate name collision. Even if hypothetically the parser can use the . between the namespace and the custom annotation name to disambiguate, you will still end up with both the namespace serialize and the builtin annotation serialize showing up in the autocomplete when you type @.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 13, 2025

Even if it is read-only and only gives an error, I think the instance should read the annotation within _valdiate_property() or set() and give an error, and the annotation side should really do nothing.

The annotation might be owned by the class, not the instance, but since what has the error is the instance, this is not strange. Rather, it is consistent with other implementations related to property.

For example, if there is get_self_annotation() and it is possible to get the annotation that targets oneself, set() can be written as follows:

@@max_length: int = 5
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().max_length)

For example, if there is get_annotation() and it is possible to get the annotation from the property name, _validate_property() can be written as follows.

@@name_type: Variant.Type = TYPE_STRING
@@max_length: int = 5
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().max_length)

func _validate_property(property: Dictionary) -> void:
	if property.name == "user_name":
		if property.type != get_annotation(property.name).name_type:
			printerr(property.name + " is not string.")

While I can understand skepticism in regards to "executing code in the editor," I don't understand why you're against creating multiple instances for the same annotation. If anything this makes them easier to use as they can have properties and work just like any other GDScript class.

I think he is saying that he is skeptical of the need to always allow @tool in order to use custom annotations on the editor rather than at runtime. Built-in annotations such as @export_range do not require it. Also, for the same verification, a single class that returns only static functions is sufficient, but there may be performance concerns if it creates instances. With static method, combined with the code I showed above, it can be written like this:

@@max_length: int = 5
@export var user_name: String:
	set(value):
		user_name = Validation.validate_length(value, get_self_annotation().max_length)

@@max_length: int = 5
@export var user_name2: String:
	set(value):
		user_name2 = Validation.validate_length(value, get_self_annotation().max_length)

validation.gd

class_name Validation
static func validate_length(str: String, max_length: int = 5) -> String:
	if max_length < 0:
		printerr("Error")
		return ""
	return str.substr(0, max_length)

The difference in memory cost becomes more noticeable as the number of user_names (members that only need length validation, not just user name) increases.

In core c++ development, the use of dictionary data is not recommended in terms of memory usage and run-time performance. However, RefCounted is also not recommended as an alternative to a dictionary.

In that case, the most commonly used type is struct. Even if we treat annotation as just metadata, if we want a certain formatting in it, we would like to treat it as a struct rather than dictionary or RefCounted (although a typed dictionary might be sufficient). So I think Struct has a higher priority than Annotation implementation. If we rush to make annotation a class, it will be difficult to change it to struct or other types.

I don't know which direction struct will go, but if it has a setter, it will allow you to achieve what you want to do with _init() in the annotation class, for example:

@define_as_annotation_with("@@") struct Annotation {
	max_length: int = 0:
		set(value):
			if value < 0:
				errprint("max_length cannot be less than zero!")
				value = 0
			max_length = value
}

@@max_length = 5
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().max_length)

If not, we can consider providing a setter for annotation externally, independent of struct.

BTW, I believe that annotation should be simpler. If we allow types other than struct for annotation, as in the code more above (like @@max_length: int = 5), I think the following would be sufficient.

struct Annotation {
	max_length: int = 0:
		set(value):
			if value < 0:
				errprint("max_length cannot be less than zero!")
				value = 0
			max_length = value
}

@@meta: Annotation = {
	max_length: 5
}
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().meta.max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().meta.max_length)

However, I am skeptical that validation (I am talking about validation by _init(), not _analyze()) is necessary for the annotation itself in the first place. Validation is required for parameters that may change dynamically. Since the annotation such as defining statically with @ that does not change dynamically, so it should not need it.

For example, if we need Annotation for Annotation, it should not be so. In other words, the variants in the extended your Annotation class should not have any Annotation like a Matryoshka. It is not a good idea to make everything too functional.

I assume that the essence of custom annotation is just a way to give meta data about the class members. The lack of further functions should not be a major obstacle to creating games at this point. A minimal implementation of a solution for adding/retrieving meta data would be an appropriate first step for custom annotation.

I suggest reading the following:
https://docs.godotengine.org/en/latest/contributing/development/best_practices_for_engine_contributors.html

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 14, 2025

Even if it is read-only and only gives an error, I think the instance should read the annotation within _valdiate_property() or set() and give an error, and the annotation side should really do nothing.

The annotation might be owned by the class, not the instance, but since what has the error is the instance, this is not strange. Rather, it is consistent with other implementations related to property.

The owner of the error is most definitely the class and not the instance. Here's why.

Consider the following case: (Demontration purposes only. I know @export_range exists)

# Range.gd
extends GDScriptVariableAnnotation

class_name Range

var _min : float
var _max : float

func _init(min : float, max : float) -> void:
  if min > max:
    error_message = "min should not be greater than max"
    return
  _min = min
  _max = max
# Test.gd
extends Node
class_name Test

@@Range(1, 2)
var x := 1.0

@@Range(5, 0)
var y := 1.0

@export
static var z := 1.0

What's causing the error in @@Range(5, 0), and what's causing the error in @export? Is it:

  1. The script Test.gd.
  2. The instance - the Object with Test.gd attached as its script. (A Node in this case)

In the case of @export, I'm sure you can agree with me that it's the script causing the error. The script declared a static variable and is trying to use @export on it, which is not supported by the @export annotation.

So it follows that for @@Range(5, 0), a custom annotation, it's also the script causing the error. The script is setting an illegal combination of arguments (5 is greater than 0) for the custom annotation. Not the object, the script.

For example, if there is get_self_annotation() and it is possible to get the annotation that targets oneself, set() can be written as follows:

@@max_length: int = 5
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().max_length)

For example, if there is get_annotation() and it is possible to get the annotation from the property name, _validate_property() can be written as follows.

@@name_type: Variant.Type = TYPE_STRING
@@max_length: int = 5
@export var user_name: String:
	set(value):
		if value.length > get_self_annotation().max_length:
			printerr("Name is too long.")
		str.substr(0, get_self_annotation().max_length)

func _validate_property(property: Dictionary) -> void:
	if property.name == "user_name":
		if property.type != get_annotation(property.name).name_type:
			printerr(property.name + " is not string.")

But there isn't, as this does not make sense. Annotations are a GDScript-only concept (have always been, even prior to this PR) and it makes no sense to introduce annotations as a concept into Objects. If anything, this would be an unexpected behavior.

I'll explain this again:

  • Export annotations are compiled into default PropertyInfos for instances.
  • PropertyInfos are per-instance, because Godot allows Objects to change, add, and remove PropertyInfos by calling virtual methods such as _validate_property and _get_property_list.
    • We can have two objects of the same GDScript class with different PropertyInfos, because PropertyInfos are per-instance.
  • Annotations themselves aren't. You can't add or remove annotations on a GDScript from an object's virtual methods. Export annotations have always been a script-level tool of convenience so average users don't have to override _get, _set, and _get_property_list to have exported (serialized) variables.
    • Godot C# also uses attributes in the same way annotations are used here.

This goes back to my point that annotations are per-class and not per-instance.

If annotations are per-instance, then truly they would have very similar purposes to PropertyInfos and would be completely redundant. And we'd have to wonder why there's no _get_annotation, _set_annotation, and _get_annotation_list virtual functions.

But they aren't. Annotations are per-class. That's the point. Because they are per-class, errors are per-class, and end up as compilation errors and not runtime errors. (e.g., in Object._get_configuration_warnings)

Yes, you can use the per-class annotations with _validate_property to validate properties like you've demonstrated here, and even printerr accordingly. This usage however is irrelevant to the self-validation (being able to check if the custom annotation itself is used incorrectly and emit compilation errors) feature.

I think he is saying that he is skeptical of the need to always allow @tool in order to use custom annotations on the editor rather than at runtime. Built-in annotations such as @export_range do not require it.

Then this is strange too. @tool is definitely not necessary to use custom annotations in the editor. @tool is not added to any of the custom annotation scripts. It's in the annotation_test.gd script because @tool is needed to allow ToolButtonEditor to call annotation_test.gd's methods. (via metadata from @@ToolButton)

With static method, combined with the code I showed above, it can be written like this:

@@max_length: int = 5
@export var user_name: String:
	set(value):
		user_name = Validation.validate_length(value, get_self_annotation().max_length)

@@max_length: int = 5
@export var user_name2: String:
	set(value):
		user_name2 = Validation.validate_length(value, get_self_annotation().max_length)

validation.gd

class_name Validation
static func validate_length(str: String, max_length: int = 5) -> String:
	if max_length < 0:
		printerr("Error")
		return ""
	return str.substr(0, max_length)

Surely you already understand this by now, but I'll say it again. Custom annotations do not and cannot validate properties on their own. That's not their purpose, and that's not why people wanted it in the custom annotations proposal thread either.

With all due respect, I do not see how this is relevant at all to this PR. It's like arguing that print should not support rich text because you can use bbcode to make it look red like printerr.

The difference in memory cost becomes more noticeable as the number of user_names (members that only need length validation, not just user name) increases.

In core c++ development, the use of dictionary data is not recommended in terms of memory usage and run-time performance. However, RefCounted is also not recommended as an alternative to a dictionary.

Putting aside the nonsensical use case, the memory cost is per-class. In Godot, scripts are only ever loaded once. This is a miniscule cost compared to how many times Nodes get instantiated.

GDScriptAnnotation inherits from RefCounted and not Object because of user-friendliness, but I'm fine with it inheriting from Object instead. The behavior is well-defined - nobody should try to "take ownership" of an annotation reference and a GDScript will delete all of its annotation objects when recompiling.

In that case, the most commonly used type is struct. Even if we treat annotation as just metadata, if we want a certain formatting in it, we would like to treat it as a struct rather than dictionary or RefCounted (although a typed dictionary might be sufficient). So I think Struct has a higher priority than Annotation implementation. If we rush to make annotation a class, it will be difficult to change it to struct or other types.

I hate to bring up other languages all the time, but C# implements System.Attribute as a reference type. Structs are value types with value semantics, (always copies instead of references) and the advantage of structs diminishes as the metadata's number of members grow.

The memory (allocation) costs of custom annotations as an Object has been vastly over-exaggerated IMO. Whether you're making a 2D platformer, a vampire survivors game with 200 enemies on the screen, or a third-person arena shooter, the memory allocation cost of custom annotations only scales with the number of usages, and is so miniscule that they can be considered part of the costs of compiling a GDScript. And of course, the cost is paid once and not per-frame.

  • I'd like to remind you that GDScriptParser already performs dynamic memory allocations for each GDScriptParser::Node (GDScriptParser::alloc_node<T>) Allocating an annotation object is miniscule compared to all the memnew calls going on there.
  • And of course, this is still considering how little this affects the amount of time it takes to compile and load a GDScript, which is already very little time.
    • The time spent on loading a GDScript is usually taken up by preloads and not the actual compilation, in my experience.

I'd even go as far as to argue that for the amount of memory you're saving on using structs instead of classes, you're now paying additional runtime costs (for copying) for however many times you're going to read from annotations. This is a dubious optimization at the cost of usability.

However, I am skeptical that validation (I am talking about validation by _init(), not _analyze()) is necessary for the annotation itself in the first place. Validation is required for parameters that may change dynamically. Since the annotation such as defining statically with @ that does not change dynamically, so it should not need it.

It does change "dynamically" as in, the user writing the code can make mistakes. Much like how we can enter an invalid path for preload, (which is analyzed in compile time by the way) a user trying to use someone else's custom annotation could make a genuine mistake when using it. (Maybe it's their first time using the custom annotation) We have everything needed to make this a compile-time error, just like errors with builtin annotations. This improves usability by a lot.

For example, if we need Annotation for Annotation, it should not be so. In other words, the variants in the extended your Annotation class should not have any Annotation like a Matryoshka. It is not a good idea to make everything too functional.

Indeed, custom annotations being classes does mean that their scripts can have annotations too. I think this is a good thing - there are no new rules introduced with custom annotations. They are just like any other class, subject to the same parsing rules. This makes them very intuitive and easy for users to pick up right away. No special annotation syntax to remember, and no "limited subset of GDScript" that they need to be aware of when extending GDScriptAnnotation.

While I foresee that "using custom annotations on a script that defines a custom annotation" is not going to be a very useful use case, I don't see any value in actively preventing the possibility either.

I think we can agree to disagree here.

I assume that the essence of custom annotation is just a way to give meta data about the class members. The lack of further functions should not be a major obstacle to creating games at this point. A minimal implementation of a solution for adding/retrieving meta data would be an appropriate first step for custom annotation.

I believe we have different ideas as to what's acceptable for this "minimal implementation."

I've mentioned this before, but I'm fine with this PR only keeping GDScriptAnnotation (abstract base class) and GDScriptVariableAnnotation, (with _is_export_annotation and related virtual methods removed) as well as the virtual methods _init and _analyze.

Does that sound acceptable to you? Or perhaps what you really prefer is just adding a Dictionary parameter to PropertyInfo? I think that's fine too, though in that case it'd be faster for me to close this PR first.


Lastly, thank you for reminding me of the best practices. I really appreciate it. 🙂

I don't have authority here, but I've made my case and I hope you can understand the thoughts that went into this design. (That's why I've spent so much time explaining every misunderstanding and addressing every concern you've had thus far) I've always acted on existing proposals, so if the decision being made here is that the proposal was fundamentally flawed/invalid - trying to solve problems that are not worth solving, then I'm more than happy to scrap this PR and stop taking up everyone's precious time.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 14, 2025

I've always acted on existing proposals, so if the decision being made here is that the proposal was fundamentally flawed/invalid

First, we will never close a proposal until some implementation has been done since the proposal is valid. If there is duplicate functionality or some solution is already available, it could be redirected and closed, but there is no easy way to add and retrieve metadata for members in Godot yet.

The proposal author's original request should be a simple request to add some metadata for each class member. And I read that it should not be "I want to add Annotation as a class" or "I want to have a functionality like _analyze() in the annotation class".

I believe we have different ideas as to what's acceptable for this "minimal implementation."

I've mentioned this before, but I'm fine with this PR only keeping GDScriptAnnotation (abstract base class) and GDScriptVariableAnnotation, (with _is_export_annotation and related virtual methods removed) as well as the virtual methods _init and _analyze.

As I said above, I disagree with the addition of _analyze(). It may be debatable about _init(), but it should be equivalent to the discussion of "whether Annotation should be treated as a class".

In my opinion, adding a class is obviously not the"minimal implementation for the purpose". As I mentioned above, there are concerns about supporting more cases such as nested Annotation within Annotation and other performance concerns.

Or perhaps what you really prefer is just adding a Dictionary parameter to PropertyInfo? I think that's fine too, though in that case it'd be faster for me to close this PR first.

Probably I do not oppose it as this PR does. However, it is not required to be associated with PropertyInfo.

I think it makes sense to manage annotation for each class, so if annotation is managed as a dictionary or simple list of Variants, and the usage is to retrieve it from within an instance and use it, as in get_annotation() as I wrote above, then I think that is the "minimal implementation for the purpose".

If we allow plain Variants in Annotation, it means that Objects can be assigned as annotation as well. Therefore, it should become some compromise that when you assign the custom annotation class you have in mind with _init() and analyze() as an object and they will work. This way, I don't see the problem with custom annotations having validation. For example:

class_name CustomAnnotationObject
extends RefCounted

var _max_length
func _init(max_length: number):
    _max_length = max_length

func validate(str: String) -> String:
    # Validate data
    return str

func analyze(property_info: Dictionary):
    # Analyze data
    pass
@@meta(CustomAnnotationObject.new(5))
@export user_name: String:
    set(value):
        if get_self_annotation().meta.validate(value):
            user_name = value

func _validate_property(property: Dictionary) -> void:
	if property.name == "user_name":
		get_annotation(property.name).meta.analyze(property)

@chocola-mint
Copy link
Contributor Author

chocola-mint commented Feb 14, 2025

I think it makes sense to manage annotation for each class, so if annotation is managed as a dictionary or simple list of Variants, and the usage is to retrieve it from within an instance and use it, as in get_annotation() as I wrote above, then I think that is the "minimal implementation for the purpose".

I still don't understand why you insist that the Object needs to have a method to retrieve its annotations.

Have we not established that annotations have always been a GDScript-exclusive concept? A method like Object.get_self_annotations is meaningless to Godot C#, where annotations don't even exist. Are you suggesting that annotations should be elevated into a core concept in Godot, shared by both GDScript and C#, accessible by any GDExtension?

What's wrong with something like object.get_script().get_annotations()?

In my opinion, adding a class is obviously not the"minimal implementation for the purpose". As I mentioned above, there are concerns about supporting more cases such as nested Annotation within Annotation and other performance concerns.

Were my answers regarding these concerns not satisfactory in the previous reply? Could you please tell me why?

I think it makes sense to manage annotation for each class, so if annotation is managed as a dictionary or simple list of Variants, and the usage is to retrieve it from within an instance and use it, as in get_annotation() as I wrote above, then I think that is the "minimal implementation for the purpose".

If we allow plain Variants in Annotation, it means that Objects can be assigned as annotation as well. Therefore, it should become some compromise that when you assign the custom annotation class you have in mind with _init() and analyze() as an object and they will work. This way, I don't see the problem with custom annotations having validation. For example:

class_name CustomAnnotationObject
extends RefCounted

var _max_length
func _init(max_length: number):
    _max_length = max_length

func validate(str: String) -> String:
    # Validate data
    return str

func analyze(property_info: Dictionary):
    # Analyze data
    pass
@@meta(CustomAnnotationObject.new(5))
@export user_name: String:
    set(value):
        if get_self_annotation().meta.validate(value):
            user_name = value

func _validate_property(property: Dictionary) -> void:
	if property.name == "user_name":
		get_annotation(property.name).meta.analyze(property)

You're suggesting that anything (any Variant, including ints, arrays, dictionaries, Objects, and any other class) can somehow be treated as an annotation. This looks and feels nothing like builtin annotations. I don't even think it's a good idea to call whatever this is "custom annotations." That'd be false advertising.

But I guess it is "minimal" in that it has zero restrictions and zero protections. Just an annotation that takes a constant-expression that evaluates to a variant, and that's what matters here.

And of course, no compile time safety nor autocomplete - not requiring a base class yet still looking for methods with special names to call is a bizarre Unity-ism if anything, so removing _analyze for such a system is the only sane thing to do.

Which means the minimal implementation would just be a builtin annotation @meta(variant : Variant) that can be used on anything. I hope this is what everyone wanted.

I don't like it personally, but I guess it's either this or nothing at all.

If you're happy with this solution instead, I'll close this PR and somebody else can implement this feature instead. At the very least it's certainly not custom annotation anymore.

@TokageItLab
Copy link
Member

TokageItLab commented Feb 14, 2025

Are you suggesting that annotations should be elevated into a core concept in Godot, shared by both GDScript and C#, accessible by any GDExtension?

If we think of annotations as meta for class members, that is what they will be. It will be available independently of annotations in other languages. Also, based on the concept that godot core is created by godot, it should be equally usable in c++, not just for GDScript.

Were my answers regarding these concerns not satisfactory in the previous reply?

While I foresee that "using custom annotations on a script that defines a custom annotation" is not going to be a very useful use case, I don't see any value in actively preventing the possibility either.

As you said about this, it would be a difference in values. I say it must be prevented from a maintainability point of view.

You're suggesting that anything (any Variant, including ints, arrays, dictionaries, Objects, and any other class) can somehow be treated as an annotation. This looks and feels nothing like builtin annotations.

In essence, it would be almost the same as godotengine/godot-proposals#1316 (comment) (the most popular reaction from people and has a positive response).

The decorator need should be discussed separately from the meta data since even if it is not a class, adding something like a function hook to the fundamental core system reduces maintainability. However, I believe it should not be a class in the least, but should be something that adds Callable to the built-in annotations like @pre_func(func: Callable = func ()) and @post_func(func: Callable = func ()).

@chocola-mint
Copy link
Contributor Author

Fantastic. Then this PR can be put to rest. At least something came out of this discussion/debate.

For any other contributor interested, it looks like the specifications would be:

  • Implement @meta(variant : Variant) as a builtin annotation that takes in any constant expression that evaluates to a Variant, and can target any class member.
  • Variants assigned to @meta should be collected by GDScriptParser and then collected by GDScript (the resource class) into an array. Or some kind of HashMap<StringName, Array> that associates each member with its metadata.
  • "Class metadata" in this form is language-agnostic. So this container can be put into the parent Script class instead of the GDScript class.
  • Metadata can then be retrieved by one of Object's methods. Maybe Object.get_metadata_list() or Object.get_member_metadata(), which would then retrieve annotations from the attached script.
  • Probably don't call this feature "custom annotation" anymore. This name is getting confusing with GDScript's builtin annotation feature.

Whoever decides to take on this task, I wish you all the best.

@AThousandShips AThousandShips removed this from the 4.x milestone Feb 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow custom GDScript annotations which can be read at runtime
5 participants