Skip to content

GDScript: Replace abstract keyword with @abstract annotation #107717

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

aaronfranke
Copy link
Member

@aaronfranke aaronfranke commented Jun 19, 2025

This was requested by @vnen and discussed in the GDScript meeting on 2025-06-17. Meeting notes:

Screenshot 2025-06-19 at 6 37 20 AM

This effectively restores the code to an earlier version of PR #67777, except that it includes abstract functions as added by PR #106409.

# Before:
abstract class_name MyBaseClass

# After:
@abstract class_name MyBaseClass

Why?

Disclaimer: I am in favor of this PR, so this list is probably biased.

Reasons For

  • Makes more sense from a language design perspective: @abstract is a modifier that modifies a class, class_name, or func. It can't be used by itself.
    • See @dalexeev's comment here for a more detailed explanation of this.
  • Arguably makes more sense from the perspective of a user reading GDScript code, since it clearly separates the keywords that declare a thing compared to the keywords that modify declared things.
    • This especially improves readability when considering syntax highlighting.
  • Consistency with possible future annotations like @override and @virtual which go well with @abstract.
  • Simpler to implement, requires a lot less special case code in the GDScript codebase.
  • Similar to some programming languages, like Python.

Reasons Against

  • Different from many languages, such as C#, Java, Kotlin, etc.
    • This may counteract the "Arguably makes more sense ..." part above, because if users are used to abstract from other languages, then abstract would be less friction. (possibly? only slightly?)
  • Some users and engine maintainers have expressed that this "feels" like it should be a keyword.
    • I forget @vnen's exact words but he said something along the lines of that this wasn't good justification and nobody could come with a clear precise reason for why this should be a keyword, while the fact of it being a modifier is an objective easy-to-measure thing.
  • Short-term: Inconsistency with static.
    • static is planned to be replaced with @static. Since removing static would break compatibility, we might add @static before removing static so both work for awhile.
  • Short-term: Affects projects running the engine's master branch and already using abstract.
    • We don't support compatibility between different revisions of master, so this is a hard breakage.
  • Short-term: Breaks the already-updated GitHub syntax highlighting for GDScript.
    • I would expect this to be resolved in a short timespan.

Neutral

  • For functions, this requires allowing the parser stage to parse bodyless functions.
    • This changes the abstract errors to analyzer errors instead of parser errors.
    • This means that a void function with no body is effectively identical to : pass except that abstract functions explicitly cannot have a body and so : pass is forbidden there and non-abstract functions must have a body.

Godot isn't a democracy, but you can react with 👍 and 👎 to show your support for or against this change.

@aaronfranke aaronfranke added this to the 4.5 milestone Jun 19, 2025
@aaronfranke aaronfranke requested review from a team as code owners June 19, 2025 14:08
@aaronfranke aaronfranke requested a review from a team as a code owner June 19, 2025 14:08
@aaronfranke aaronfranke force-pushed the abstract-annotation branch 2 times, most recently from be946d4 to 7767d3f Compare June 19, 2025 14:35
@dalexeev
Copy link
Member

dalexeev commented Jun 19, 2025

and it also un-bumps the bytecode version number that was bumped by PR #106552

There is #82808, so there is no need to un-bump.

@romgerman
Copy link

Hey! This PR feels more complete than #107713 because it has an explanation. I understand that it's "too late" to be trying to stop the moving train but still I would like to express my thoughts.

I'm all for UX, consistency, minimalism, readability and legibility but I must say these reasons don't really justify the change IMO.

I agree with the fact that abstract keyword is a modifier, an access modifier, that cannot be used my itself. But I'm not sure if I can agree with using this fact as a reason for the change. There are other keywords in GDScript that cannot be used by themselves like extends and static.

From readability standpoint (keyword vs annotation) I think it really depends on the user, on how they perceive code and how they write it.

What I really want to say is this. This is my opinion and a view of how I perceive annotations in GDScript:
Annotations do not change how user interacts with code (coding experience? I'm not really sure how to call it). There is currently no annotations which add conditions (constraints) in between the user and their code that dictate them how to write it. All of current annotations change external behaviors like how exported variables look inside the editor, how node script appears in the scene tree, does a property gets stored in a file or not, and so on. The only exception that comes close is perhaps @ready, though that's debatable.
But then when you use @abstract annotation on:

  • a class, then you cannot instantiate that class;
  • a class method, then you must override that method in the child class;
  • a class method in a class that has no @abstract, then it will produce an error unless you annotate that class too.
    And then other annotations like @virtual and @override are going to change this further.

That was what I wanted to say. If annotations are moving towards becoming a more generic system (allowing users to create their own like decorators in JS, attributes in C#, annotations in Java, etc.) then I'm all for it.

@produno
Copy link

produno commented Jun 19, 2025

I have always assumed annotations are used for anything that affect the editor in some way and keywords were for things that affect the code. So this change seems confusing to me.

I shall give some quick (vague) examples:

@export - Shows in the inspector panel. (Interacts with the editor)
@icon - Gives the class an icon. (Interacts with the editor)
@tool - Allows the class to run in the editor. (Interacts with the editor)
@warning_ignore - Suppresses warnings in the editor. (Interacts with the editor)
@onready - Retrieves the node path from within the scene tree upon the node being ready. (interacts with the editor)

class_name - Declares a global class name for the script. (Interacts with the code)
static - Defines a class level var or func. (Interacts with the code)
extends - Declares inheritance. (Interacts with the code)
etc.

I think having abstract as an annotation deviates from this and is a little confusing.

@jordedwards
Copy link

jordedwards commented Jun 19, 2025

I somewhat disagree with the above comments, I don't think it's fair to say that annotations are exclusive to the editor.

@export and @ready can both fundamentally change the behaviour of a variable/property as they affect its lifecycle, just because this part is "hidden" and due to the VM interpretation (or in the case of C#, injection of values into the class instance) they are still "core" to the language behaviour.

My only gripe with the reasoning outlined here is that, following the "Reasons For", doesn't this also apply to the static keyword? I'm struggling to see exactly why "abstract" would fall into the annotation bucket while "static" wouldn't fit the same criteria here.

I do think this reason

Simpler to implement, requires a lot less special case code in the GDScript codebase.

is quite valuable, simplicity of the underlying code is immensely valuable, especially in a project where open contributions are encouraged and things like this can improve coprehensibility.

@geekley
Copy link

geekley commented Jun 19, 2025

Honestly, to me I already accepted that the difference between keyword and annotations in GDScript currently is too arbitrary anyway, so I'm like... whatever. I'd normally prefer keyword, but if virtual and override will be annotations, I prefer consistency more.


However, if there's ever a GDScript 3.0 (5.0?) on Godot 5, please make the annotation system more meaningful, with annotations only for things that need it, have a more clearly defined reason for annotations to exist.

I think everything in the language where its presence/absence check is mandatory (i.e. that breaks forward-compatibility) should be a keyword, even if it has functionlike(...) syntax.

Then make annotations some extensible system allowing custom classes and some hook system to modify the compilation generically like a preprocessor (but syntax-based, not text-based like C).
E.g. @MyCustomAnnotationClass(...) or @InBuiltOptionalAnnotation(...) that could generically use reflection, modify code about to be emitted, raise custom warnings or errors, all via GDScript code. By just adding the class/addon you'd get support for that behavior.
You could make it so e.g. UpperCase first letter means it's optional (forward-compatible, so unrecognized ones are silently ignored) and lowercase is mandatory check, e.g. @custom_annotation(...) would raise error if unrecognized (so not forward-compatible).

This would allow some GDScript features be introduced by addons, reducing some implementation weight on core devs. Core features could be introduced as inbuilt annotations when they can be forward-compatible, and as keyword otherwise. It would also facilitate testing these features by many users without recompiling the engine.

@produno
Copy link

produno commented Jun 19, 2025

virtual and override have not yet been introduced, and according to the link you sent me in a previous discussion here, they will be decided upon on a case by case basis.

It seems that contributors could not decide upon a consensus, so we all have to suffer the consequences of a mish mash design. Although it already seems like there's a pattern for what an annotation is or is not, so why not continue with that.

I guess that's unfortunately one of the issues with open source development.

Co-authored-by: Danil Alexeev <dalexeev12@yandex.ru>
@aaronfranke aaronfranke force-pushed the abstract-annotation branch from 7767d3f to bdeb83d Compare June 19, 2025 23:07
@aaronfranke
Copy link
Member Author

@dalexeev Dang, we worked on the same thing. I said in the GDScript meeting that I would work on this. Un-un-bumped, and I updated my PR to match the more detailed documentation in your PR. You and I took different approaches, though. I initially dismissed the approach in your PR because it's pretty much only a syntax change, and still keeps the "special case" code in the language parser. The approach in this PR is to make it behave like other annotations, which for functions, does involve moving the body-or-not check to the analyzer stage.

@romgerman extends can be used by itself, because class names are optional in GDScript. Also, @abstract is effectively editor tooling, since we don't need to check for instantiating abstract classes at runtime.

@romgerman @jordedwards The plan is also to replace static with @static. See the in-progress discussion in the GDScript channel on Godot's RocketChat for more information.

@aaronfranke aaronfranke changed the title GDScript: Replace abstract keyword with abstract annotation GDScript: Replace abstract keyword with @abstract annotation Jun 19, 2025
@aaronfranke aaronfranke requested review from vnen and dalexeev June 19, 2025 23:10
@jordedwards
Copy link

@aaronfranke gotcha, sorry for not clarifying before assuming static was treated differently

@JoNax97
Copy link
Contributor

JoNax97 commented Jun 20, 2025

I think static is the best argument against @abstract being an annotation.

Both are modifiers, both apply to both classes and methods, and both are language-level features, that don't interact with other parts of the engine (like most annotations do, as @produno said).

I understand the team's reluctance to add more keywords to the language, 3.X went too far into that direction and the ones that were turned into annotations in 4.0 are a good change. But I think this is going too far into the other direction.

@OhiraKyou
Copy link
Contributor

Here's how I've always interpreted the responsibilities of keywords and annotations:

  • Keywords are for language features.
  • Annotations are for adding custom features based on where the language is being used (e.g., a game engine or specific game project). Godot simply provides some out-of-the-box annotations to interface with the engine and editor.

As such, I fundamentally disagree with abstract, static, virtual, or override being annotations. I know that GDScript is developed specifically for Godot. But, I believe that the past language and implementation division of keywords and annotations was wise, even if it was unintentional.

@Lazy-Rabbit-2001
Copy link
Contributor

Lazy-Rabbit-2001 commented Jun 20, 2025

Based on vnen and my thought, the most reason why we treat some modifiers as annotations is because of the frustrating system of adding new keywords. Every time you add a keyword, you have to change a lot of files, even the core files of gdscript module. Don't believe it? Check the source file and see how these keywords work.

Meanwhile, the handle of keywords is much more difficult than annotations, which just require registration and implementation method during their definitions.

Adding more keywords means higher unreadability of the source code, increasing the difficulty of maintaining the engine module. Maybe we can dig deeper on this issue in GDScript 3.0 in the future and overhaul the whole keyword system to make it much more simpler to handle.

As for static being @static, we've discussed and agreed to keep both in the engine, while the keyword static will be deprecated with a warning. Dislike warnings? We are considering the update tool to help with converting all static keywords to annotations.

I admit that it is still ambiguous for most of us when a syntax element is a keyword and when it is presented as an annotation. But for modifiers in the middle of the both, we have to discuss more and reach a clear and determined consensus on this question.

@OhiraKyou
Copy link
Contributor

OhiraKyou commented Jun 20, 2025

All keywords modify some behavior in some way. Otherwise, they would just be white space. So, the very idea of modifiers as a distinct category is vague.

I much prefer the distinction between language features (provided by keywords) and engine features (exposed to the language through annotations). Annotations act as an extension system for the language, enabling context-specific features (i.e., those from an engine or game) to be injected into the language. Naturally, adding an extension is easier than modifying core files. If keyword implementation could be streamlined, that's a separate matter.

And, indeed, if keyword implementation is difficult, and a conversion is to be made, it makes far more sense to convert from annotation to keyword rather than from keyword to annotation. One could implement a language feature as a prototype annotation and then replace it with a keyword when it's ready.

@produno
Copy link

produno commented Jun 20, 2025

In fear of flogging a dead horse, I shall make one last comment.

As an end user, and selfishly so, I do not care how complex something is to implement and maintain. All I care about is the end user experience being as good as it possibly can be. I don't think usability should be sacrificed for less complexity, or at least, if such an integral part of the experience is difficult to maintain, then I guess that's an issue within itself. (Of course I understand this is not the case for everything.)

That said, I will respect the decision of the maintainers. I will just have nightmares of annotations littering my codebase.

@Ivorforce
Copy link
Member

Ivorforce commented Jun 20, 2025

I'll note something that I think has not been said yet. To me, the main motivation to make abstract a modifier rather than a keyword is this:

abstract does not affect the language grammar.

Making it a keyword forces parsers to resolve it immediately when building the syntax tree. This can bring problems, such as missing syntax highlighting, broken autocomplete, or unhelpful error messages while editing the code.

To provide the best user experience, the language grammar needs to be agnostic to contextual details, such as whether a function needs a body. That type of question is better answered during type and variable resolution.

I think some people feel strongly about abstract being a keyword because it is a keyword in many other languages. But when taking a step back, the best choice is obvious. The effect of making language parsers easier to implement, and more forwards compatible, is secondary (though welcome).

@AThousandShips
Copy link
Member

AThousandShips commented Jun 20, 2025

Beyond a few early adopters using 4.5.dev5 I don't see any usability issues with changing this from a keyword to an annotation, so there isn't really a trade off between code complexity etc. and user experience I can see (and while we should reduce friction as much as possible the development versions are unstable and that should be well known, so some friction is to be expected)

@OhiraKyou
Copy link
Contributor

OhiraKyou commented Jun 20, 2025

If the parser and variable resolution can't elegantly handle an abstract keyword, that sounds like a problem with the parser and variable resolution.

The usability issues, or friction, of an @abstract annotation comes from using a core language feature as if it were a context-specific (i.e., engine) feature injected into the language.

@geekley
Copy link

geekley commented Jun 20, 2025

Y'all should just have been using ANTLR4 to make the parser instead of coding it by hand...
It's far easier to design the language, less bug-prone, open-source and just as efficient as you'd expect.
Plus, designing the grammar in ANTLR BNF-like language would help with external tools like IDEs supporting it.

@aaronfranke
Copy link
Member Author

To @Ivorforce's point, here is an example of how error messages change as of this PR. In master, this code:

abstract abstract class A:
	pass

gives the error Expected "class_name", "extends", "class", or "func" after "abstract"., while this code:

@abstract @abstract class A:
	pass

gives the error "@abstract" annotation can only be used once per class.. This error seems to be more direct, and I'd argue this is an improvement, but it can be argued either way. ¯\_(ツ)_/¯

The error gives a hint to how this simplifies the parsing of the language: it only has to keep track of annotations and apply them later, instead of having to check for every possible thing that can come after abstract. In this case, it knows that @abstract can only be used once per class, so it just says that when applying annotations to the parsed class, instead of failing before it begins to parse the class.

@OhiraKyou
Copy link
Contributor

If abstract needs to be ignored by the parser and applied later, then abstract should be ignored by the parser and applied later. Again, this sounds like a deficiency of the parser and resolution rather than a justification for using an annotation.

Imagine if someone said "the implementation workflow and error reporting for the and keyword sucks, so it should be @and instead".

@Ivorforce
Copy link
Member

If abstract needs to be ignored by the parser and applied later, then abstract should be ignored by the parser and applied later. Again, this sounds like a deficiency of the parser and resolution rather than a justification for using an annotation.

Imagine if someone said "the implementation workflow and error reporting for the and keyword sucks, so it should be @and instead".

At this point, it would already work exactly like an annotation (minus the forwards compatibility), and we'd just be arguing over whether we want the leading @ or not. Is it important to omit it if it works the same way? I personally think it doesn't matter, but having the @ is more truthful to how it actually works grammar-wise.

@RumarioVR
Copy link

abstract is not an annotation. The most important thing in language design is to be consistent. Then it must also be called @class_name. Not a good solution, in my opinion. But I am happy that it finally exists 😄

@romgerman
Copy link

This can bring problems, such as missing syntax highlighting, broken autocomplete, or unhelpful error messages while editing the code

This seems to be an internal issue in the scripting system. This shouldn't lead to compromises in language consistency.

@OhiraKyou
Copy link
Contributor

At this point, it would already work exactly like an annotation (minus the forwards compatibility), and we'd just be arguing over whether we want the leading @ or not. Is it important to omit it if it works the same way?

Yes, it's important. Indeed, I posit that the average user arguing for it being a keyword—myself included—doesn't actually care how it's implemented in the backend as long as it is typed like a keyword. So, omission of the @ is at the heart of the argument.

If I wanted to write an abstract class in Godot, my first instinct would be to type abstract. If I found out that an @ was required, my immediate response would be to question why that is and search for a well-documented, consistent reason for the difference. If I didn't find one, I would end up finding a place to make the same argument I have here: that the difference between keywords and annotations should be whether they are features of the language or features injected from a context (i.e., engine or game).

having the @ is more truthful to how it actually works grammar-wise.

Quirky implementation details don't need to bleed into user space. @ or no @ is a UX problem.

@dalexeev
Copy link
Member

I think the discussion is going in the wrong direction. We decided to replace the abstract keyword with the @abstract annotation primarily for language design reasons, not because of parser limitations.

You can think of annotations as attributes on class members. A class consists of different kinds of members (variables, functions, signals, constants, etc.). Each member can have additional attributes, which are specified in annotations. This makes sense from the grammar of the language. Declarator keywords (var, func, signal, const, etc.) answer what a class member is (nouns). Annotations answer which a class member is (adjectives). The grammar of the language and the parser should only be responsible for building the abstract syntax tree. They don't care whether you use self inside a static method or not, that's the responsibility of the static analyzer and compiler.

We discussed several options for the separation criterion between keywords and annotations:

  1. Keywords for common language features, annotations for Godot-specific things.
    • It's too debatable. How many languages ​​should have a given keyword for it to be common? Also, we would not want to unreasonably increase the number of keywords due to possible collisions with user identifiers. In addition to abstract, in the future we may get virtual, override, final and other modifiers. Also, all other things being equal, we prefer simplicity of implementation, even if this is not the most important factor in making a decision.
  2. Keywords for things that affect execution/VM, annotations for editor-only and analyzer-only metadata.
    • This rule is already not followed in a major way (@onready postpones variable initialization until _ready() is called, @rpc changes the behavior of functions in multiplayer, export annotations affect serialization and duplication of an object, etc.) and is also quite debatable. For example, there was a proposal to replace @onready with onready.
  3. Keywords for declarators, annotations for their modifiers.
    • This rule is not followed for static and that many users are used to the fact that in other languages ​​these modifiers are keywords, not annotations/attributes/decorators (probably because the latter appeared in languages ​​later). However, at least this criterion is the clearest and would allow us to save ourselves from case-by-case debates about whether the next modifier should be a keyword or annotation.

@JoNax97
Copy link
Contributor

JoNax97 commented Jun 20, 2025

Ok, even though I don't personally like it, that is sound logic. Maybe we're just too used to keywords in other languages.

@romgerman
Copy link

I think the discussion is going in the wrong direction

I'm glad there is a discussion. It's important because this is not something that can be easily reverted back. But I agree that there are enough subtleties for everybody to make this topic endless. I'm okay with the change.

not important but funny

not because of parser limitations
this is not the most important factor in making a decision

But then

  • Simpler to implement, requires a lot less special case code in the GDScript codebase

frustrating system of adding new keywords

Making it a keyword forces parsers to resolve it immediately when building the syntax tree. This can bring problems

this simplifies the parsing of the language

It seems like the parser is a problem if every reasoning consists of "well if it weren't for parser".

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.