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

Fix GDScript base and outer classes, signals and functions lookup order #70246

Merged
merged 1 commit into from
Jan 10, 2023

Conversation

adamscott
Copy link
Member

@adamscott adamscott commented Dec 18, 2022

Fix GDScript inner and outer classes lookup order

  • Add outer class lookup test
  • Add signal lookup test

Fixes #70216 (again)
Fixes #70495 - Accessing keys() dict of an enum defined in a singleton crashes app

@adamscott adamscott requested a review from a team as a code owner December 18, 2022 05:48
@vonagam
Copy link
Contributor

vonagam commented Dec 18, 2022

Hah, but unfortunately not right. There is difference in logic for base and outer classes. In base classes they do handle variables, functions and signals, while in outer lookup it is ignored.

So, the next example based on that:

class A:
 var Q := 'right one'

class X:
 const Q := 'wrong one'

class Y extends X:
 class B extends A:
   func check() -> void:
     print(Q)

func _ready() -> void:
 var b := Y.B.new()
 b.check()

@adamscott
Copy link
Member Author

Hah, but unfortunately not right. There is difference in logic for base and outer classes. In base classes they do handle variables, functions and signals, while in outer lookup it is ignored.

So, the next example based on that:

class A:
 var Q := 'right one'

class X:
 const Q := 'wrong one'

class Y extends X:
 class B extends A:
   func check() -> void:
     print(Q)

func _ready() -> void:
 var b := Y.B.new()
 b.check()

Please. I do please you to create a complete example in advance.

@vonagam
Copy link
Contributor

vonagam commented Dec 18, 2022

Yeah, sorry for that, my reasoning is that I try to show the simplest example that everyone will see is wrong, not the most convoluted one (because it will be confusing and less in priority).

When you present a solution, I look at it and analyze what would be the next simplest showcase that the solution is wrong.

I also pointed out the root of the problem - as I wrote in the issue opening text you cannot continue using get_class_node_current_scope_classes just for outer logic. So when the known root of the problem is not addressed it is quite easy for me to come up with next example.

@adamscott adamscott force-pushed the fix-class-lookup-redux branch 2 times, most recently from 48d7eb8 to dcbccbc Compare December 18, 2022 06:32
@adamscott

This comment was marked as off-topic.

@adamscott adamscott force-pushed the fix-class-lookup-redux branch 5 times, most recently from a7dffc3 to 627d8d9 Compare December 18, 2022 07:12
@vonagam
Copy link
Contributor

vonagam commented Dec 18, 2022

please add some context or a solution yourself

I think I provided the context, but solution is not a one-line change. My thoughts: I think that get_class_node_current_scope_classes has value and to be consistent with other places it should be used to replace whole while (base_class != nullptr) loop and in iteration you need to check if the current script is outer or not, if it is base one then instance things like variables/functions/signals should be accepted, otherwise not.

@Chaosus Chaosus added this to the 4.0 milestone Dec 18, 2022
@adamscott adamscott marked this pull request as draft December 18, 2022 12:27
@adamscott adamscott force-pushed the fix-class-lookup-redux branch 2 times, most recently from 27bbe15 to 794c7ff Compare December 18, 2022 13:34
@adamscott
Copy link
Member Author

@vonagam I replaced the list value of GDScriptParser::ClassNode * with a new list of GDScriptAnalyzer::CurrentScopeClass structs. It adds metadata, as you suggested, to know if the current class node "type" (I'll probably rename it) is BASE or OUTER.

Then, I realized that get_class_node_current_scope_classes() in reduce_identifier_from_base() already loops through the base, so I removed the while (base != nullptr) to introduce a single if (base != nullptr) condition.

@rune-scape @anvilfolk If you could add your input to this complex issue, it would be greatly appreciated.

@anvilfolk
Copy link
Contributor

anvilfolk commented Dec 18, 2022

Been looking at this a little bit, and I'm starting to wonder if maybe we shouldn't make the analyzer a little more flexible. What if we had a bitfield in reduce_identifier_from_base and the like, which defines which types of things are in scope?

I can imagine this bitfield having:

  • CLASS_TYPES, such as inner classes,
  • CLASS_MEMBER, such as static functions & enums
  • INSTANCE_MEMBER such as variables and non-static functions), and ALL for everything.
  • ALL all of the above

This way, depending on what we are searching for (e.g. whether we're looking into base classes or outer classes), we could pass the method some information stating that it should ignore instance members and class types, and only look for class members.

Would this make sense at all?

@vonagam
Copy link
Contributor

vonagam commented Dec 18, 2022

Alternative solution to separate bases from outer:
get_class_node_current_scope_classes currently returns base classes first then outer ones. So if you get a number of base classes either by returning or passing int by reference then by comparing if current index is less than a number of base classes or not you will know it's type. That's an option that requires no additional types/structures, but just an option.

@adamscott
Copy link
Member Author

adamscott commented Dec 18, 2022

get_class_node_current_scope_classes currently returns base classes first then outer ones.

I think you misunderstand the method.

It returns, yes, base classes in priority over outer classes. But the recursive nature of the method makes it so that outer classes of the first base class have priority over the second base class.

@adamscott
Copy link
Member Author

It returns, yes, base classes in priority over outer classes. But the recursive nature of the method makes it so that outer classes of the first base class have priority over the second base class.

Whoa, I'm totally wrong. You're right!

@adamscott
Copy link
Member Author

Just found out how to add support for signals. It's in the PR.

@adamscott adamscott force-pushed the fix-class-lookup-redux branch 3 times, most recently from 26ad979 to 1851996 Compare December 18, 2022 18:52
@adamscott
Copy link
Member Author

@vnen @vonagam I rebased the PR with the merge of #70613.

I think the PR is ready.

@adamscott adamscott force-pushed the fix-class-lookup-redux branch 5 times, most recently from d93c284 to 62df74b Compare January 4, 2023 01:29

p_identifier->set_datatype(p_identifier_datatype);
Error err = OK;
GDScript *scr = GDScriptCache::get_full_script(p_identifier_datatype.script_path, err).ptr();
Copy link
Member

Choose a reason for hiding this comment

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

There's a bit of an issue about loading the script in the middle of the analyzer because this will not work if there are cyclic dependencies between the classes (that is, the script being loaded depends on the one being parsed, this will fail).

I'm not sure why this needs to be made a compile-time constant. I can see some benefits but I don't know if it can work properly in all cases so it might be better for the compiler to sort this out in the next phase.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's a bit of an issue about loading the script in the middle of the analyzer because this will not work if there are cyclic dependencies between the classes (that is, the script being loaded depends on the one being parsed, this will fail).

For now, the tests pass and no issue about this has been found yet. Do you have an idea of a test to add where the issue would happen?

I'm not sure why this needs to be made a compile-time constant.

I added the inlining as it's just a set of instructions to execute, there's no logic beyond this function, like there's no need for this function other than reduce duplicated code. But I'm kinda a noob at C++, so I can remove the inline part.

Copy link
Member

Choose a reason for hiding this comment

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

Looking at the code deeper now I understand why this "works". get_full_script() actually returns the shallow script here, since this is not asking to update from disk. The current script being loaded is already on the full_gdscript_cache because it 's added to the map before it's actually compiled, so the get_full_script() will never fail, but sometimes return a shallow (i.e. not compiled) script instead.

So technically this will always work because even if it can't compile due to a reference cycle, it will still get the script that will be compiled later.

The main problem now is that you add a reference to the script in the function's list of constants, which will create a reference cycle that can never be cleared. Especially since this is also done for the very script being compiled now, so any reference to the same class will create this ref cycle (noted, it's not often that one references the same class by name, but there's no reason to keep a new reference to same script if that happens).

My question is still the same though: why does this need to be a compile-time constant? Even if you are accessing members from it, I believe the analyzer is able to retrieve the members directly without having to store the class. For instance: var v = ClassName.InnerClass.SOME_CONST can just grab SOME_CONST and store it directly as a constant. With this PR the InnerClass would also be stored in this case, which is unnecessary.

Unless I'm missing something else, making this constant does not serve any particular purpose and it's not needed in any case.

Copy link
Contributor

@vonagam vonagam Jan 9, 2023

Choose a reason for hiding this comment

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

It started with need to make const X := preload(...).X work. If a subscript for an inner class is not constant then it cannot be assigned to a constant right now. And if it is a constant then it is expected to have reduced_value. #70055 is where it was fixed like that.

Copy link
Member

Choose a reason for hiding this comment

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

My issue is that with a script like this:

class_name Foo
const x = 0
func bar():
	print(Foo.x)

You now store a reference to the Foo script inside the constants of its own function bar. I understand storing references to other scripts (even if they make a cycle) but I don't see how this is necessary.

At the very least this should avoid storing the very same script being compiled.

Copy link
Member

@vnen vnen left a comment

Choose a reason for hiding this comment

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

The overall approach is good, I just have the one concerned I commented about loading scripts during parsing.

@adamscott adamscott force-pushed the fix-class-lookup-redux branch 4 times, most recently from 44661b1 to 18c653a Compare January 8, 2023 21:13
@adamscott
Copy link
Member Author

The build fails, but it's Windows that failed to build the artifacts, not that it had errors elsewhere.

@akien-mga akien-mga requested a review from vonagam January 9, 2023 08:48
Copy link
Contributor

@vonagam vonagam left a comment

Choose a reason for hiding this comment

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

I'm coauthoring (for part inside reduce_identifier_from_base), so do approve.

Though, it is bugging me that get_full_script is used for getting a script for a class and not get_shallow_script with raise_status as it is done for the same purpose in preload. But at this point it is already a preexisting thing, this PR extends what this logic applies to but does not change it. I do hope that in future shallow script can be made to work.

Otherwise everything is perfect.

@adamscott
Copy link
Member Author

and not get_shallow_script with raise_status as it is done for the same purpose in preload.

I did not try that, I'll try!

@adamscott
Copy link
Member Author

As just discussed with @vonagam, get_shallow_script() returns errors and doesn't pass the tests. But it works in GDScriptAnalyzer::reduce_preload();.

The raise_status part that he underline is more complex than I anticipated.

So, as he suggested, another PR should maybe take a look at why and fix it.

modules/gdscript/gdscript_analyzer.cpp Outdated Show resolved Hide resolved
Copy link
Member

@vnen vnen left a comment

Choose a reason for hiding this comment

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

I believe we can merge as it is since it works and fixes the issues. I still have concerns about the lifetime of the scripts since I believe there are some cases creating reference cycles, but we can solve those problems (if they do exist indeed) properly later on.

- Add outer class lookup test
- Add signal lookup test

Co-authored-by: Dmitrii Maganov <[email protected]>
@akien-mga akien-mga merged commit 70b24e2 into godotengine:master Jan 10, 2023
@akien-mga
Copy link
Member

Thanks to both of you, huge work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants