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

Implement typed dictionaries #78656

Merged
merged 1 commit into from
Sep 6, 2024
Merged

Conversation

Repiteo
Copy link
Contributor

@Repiteo Repiteo commented Jun 24, 2023

This aims to add the ability to bind typed dictionaries to script & GDScript. The implementation takes heavy inspiration from the existing typed array format. The primary difference is the ability to specify type for just a key, leaving the value as typeless much like current dictionaries. Both key and value need their types specified for typed dictionaries, with Variant allowing for a typeless equivalent.

Syntax

var typed_key_value: Dictionary[int, String] = { 1: "first value", 2: "second value", 3: "etc" }
var typed_key: Dictionary[int, Variant] = { 0: "any value", 10: 3.14, 100: null }
var typed_value: Dictionary[Variant, int] = { "any value": 0, 123: 456, null: -1 }

Editor

23-06-26 10-02-25 godot windows editor dev x86_64 san

Issues

All core issues are resolved!
There are a fair number of areas that still require fixes, in addition to whatever lingering bugs/errors there are. In particular:

  • No way currently to make a typeless key and a typed value
  • Variant text appears as white instead of green in GDSCript if used as the value:
    23-06-26 10-18-36 godot windows editor dev x86_64 san
  • Editor fails to update creation key/value sections if changing type
  • Editor incorrectly perceives the initial typed key/value as null
  • Editor fails to setup exported dictionaries during runtime
  • Editor won't recognize a typed key passed with the incorrect type as invalid in a GDScript function (that is, when using the [] operator); it throws an error if attempting to do so in runtime & does recognize invalid keys when creating a dictionary initially
  • Editor will treat any Object variant as a "Resource" regardless of provided type
  • Dictionary fails to recognize local script types in curly-bracket initialization during runtime
  • Potential memory leak with container_element_type being a list now instead of a single item. (This is inexperience with the engine on my end, dunno if it needs something special to memdelete array entries)
  • Implementation of GDScript tests

Closes godotengine/godot-proposals#56

@lufog
Copy link
Contributor

lufog commented Jun 25, 2023

I would prefer:

var my_dictionary: Dictionary[int, Variant]
var my_dictionary: Dictionary[Variant, int]

Just Dictionary[int] a little confusing.

core/object/object.h Outdated Show resolved Hide resolved
@Repiteo
Copy link
Contributor Author

Repiteo commented Jun 25, 2023

@lufog Great point! I adjusted the code to handle exactly that, but it revealed some other pitfalls in the process (mainly the process of transfering container types from one DataType to another) so I'll have to iron that out before showing off a new version

In the meantime I've added documentation descriptions & made it possible to use a constructor to make a typed dictionary in GDScript, much like typed arrays

@YuriSizov YuriSizov added this to the 4.x milestone Jun 26, 2023
@Repiteo Repiteo force-pushed the typed-dictionary branch 3 times, most recently from 8b50554 to 6c41e2e Compare June 28, 2023 20:13
@Repiteo
Copy link
Contributor Author

Repiteo commented Jun 28, 2023

GDScript tests implemented! They're largely lifted from the tests given to typed Arrays so more specialization will be required, but they're a solid starting point to see what's awry off the bat

Namely, typed arrays seem to be instantiating with null keys & values, though they're recognized as typed. I'm not quite sure yet where the incorrect implementation is happening, but that's my current focus as it very well may be the linchpin of the editor errors

@Bromeon
Copy link
Contributor

Bromeon commented Jun 29, 2023

With typed arrays, we have the variance problem:

var typed: Array[int] = [1, 2, 3, 4]
var as_untyped: Array = typed

as_untyped[0] = "string" # silently fails at runtime

I guess the same problem exists here, but for both keys and values?
How do we deal with that?

@dalexeev
Copy link
Member

With typed arrays, we have the variance problem

The exception for Array (aka Array[Variant]) was intentionally added as a convenience to users of untyped GDScript.

var a: Array[Node2D]
var b: Array[Node] = a # Error
var c: Array[Variant] = a # Unsafe, but ok.
var d: Array = a # Unsafe, but ok.

@Bromeon
Copy link
Contributor

Bromeon commented Jun 29, 2023

The exception for Array (aka Array[Variant]) was intentionally added as a convenience to users of untyped GDScript.

Yes, but it's still a hole in the type system. I understand the desire to pass typed arrays to APIs accepting untyped ones, and for reading, this is completely sound. The covariance violation occurs when writing to an Array that points to a Array[T]. This cannot be checked at parse time, it's a runtime check (and IIRC it's silent do-nothing, no error at the moment).

My question is, is the same covariance conversion ("upcast") planned for dictionaries on each of the generic parameters? That is:

  • Dictionary[int, String] -> Dictionary[int, Variant]
  • Dictionary[int, String] -> Dictionary[Variant, String]
  • Dictionary[int, Variant] -> Dictionary[Variant, Variant] == Dictionary

And if yes, are we fully aware of the pitfalls?

@dalexeev
Copy link
Member

This cannot be checked at parse time, it's a runtime check (and IIRC it's silent do-nothing, no error at the moment).

However, this is not directly related to the PR. PRs of this kind tend to garner large numbers of comments that are difficult to navigate. If you'd like to discuss this, I suggest the #gdscript channel on Rocket Chat.

@Bromeon
Copy link
Contributor

Bromeon commented Jun 29, 2023

Thanks! My point was not to discuss typed arrays, but the covariance problem regarding dictionaries, and what conversions we allow (see above). As such it's quite on-topic I believe.

@Repiteo
Copy link
Contributor Author

Repiteo commented Jun 29, 2023

The holes in the type system certainly have been a bit of a headache to navigate, but attempting to overhaul that system as a whole is probably a bit beyond this PR (at least as I initially envisioned it). This is more about setting up the system that currently exists to dictionaries, warts and all. If the current implementation were to somehow inhibit the ability to implement typed dictionaries in the first place, then changes to the system would make sense

On that note, I've figured out why variants were appearing as null & not changing when swapping types, so both those fixes are in place. As it stands, the two biggest remaining issues are:

  • The editor treats any Object variant as "Resource", regardless of the provided type (need to figure out how typed arrays handle this)
  • A dictionary fails to properly parse provided key/value if there's unorthodox syntax (eg: typed_dictionary_assign_wrong_to_typed.out has the wrong error, thinks an entirely different type is passed on as the value (from reading the key?))

@Repiteo Repiteo force-pushed the typed-dictionary branch 3 times, most recently from 39e959b to 56daf46 Compare July 2, 2023 20:13
core/doc_data.cpp Outdated Show resolved Hide resolved
core/doc_data.cpp Outdated Show resolved Hide resolved
editor/doc_tools.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
doc/tools/make_rst.py Outdated Show resolved Hide resolved
modules/gdscript/gdscript_disassembler.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_disassembler.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_disassembler.cpp Show resolved Hide resolved
modules/gdscript/gdscript_function.h Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.cpp Outdated Show resolved Hide resolved
@Repiteo Repiteo force-pushed the typed-dictionary branch 2 times, most recently from 52c0bb0 to ce7009f Compare August 30, 2024 17:10
Copy link
Member

@dalexeev dalexeev left a comment

Choose a reason for hiding this comment

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

Overall it looks good to me, the implementation is consistent with typed arrays. We debated for a long time whether we should resolve the typed array issues first, but then we came to the conclusion that waiting would take too long. So if we have to break typed collection compatibility in the future, it will be both arrays and dictionaries.

In my opinion, this PR is already in a very good state to be merged. However, it's worth waiting for approval from @vnen as the main GDScript maintainer.

Of course, there are some things we might have missed or overlooked, but I think we could address them in follow-up PRs.

  1. Attribute access does not have static checks, unlike index access. See the review comments.
  2. Add support for binary serialization of typed dictionaries, like in Core: Add typed array support for binary serialization #78219. Since this is a sensitive part, it's good to do in a separate PR.
  3. PROPERTY_HINT_DICTIONARY_TYPE vs PROPERTY_HINT_TYPE_STRING. These hints have different formats and are sometimes mixed up in the engine.
  4. The inspector part can be improved in the future. Currently, keys are displayed as string representations, which is not very convenient for viewing and editing.
  5. Perhaps it makes sense to adjust the documentation, GDScript tests, disassembler, etc.

Thanks for your great work, @Repiteo!


Patch
diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd
index 5d615d8557..1e2788f765 100644
--- a/modules/gdscript/tests/scripts/utils.notest.gd
+++ b/modules/gdscript/tests/scripts/utils.notest.gd
@@ -24,6 +24,11 @@ static func get_type(property: Dictionary, is_return: bool = false) -> String:
 				if str(property.hint_string).is_empty():
 					return "Array[<unknown type>]"
 				return "Array[%s]" % property.hint_string
+		TYPE_DICTIONARY:
+			if property.hint == PROPERTY_HINT_DICTIONARY_TYPE:
+				if str(property.hint_string).is_empty():
+					return "Dictionary[<unknown type>, <unknown type>]"
+				return "Dictionary[%s]" % str(property.hint_string).replace(";", ", ")
 		TYPE_OBJECT:
 			if not str(property.class_name).is_empty():
 				return property.class_name
@@ -188,6 +193,8 @@ static func get_property_hint_name(hint: PropertyHint) -> String:
 			return "PROPERTY_HINT_INT_IS_POINTER"
 		PROPERTY_HINT_ARRAY_TYPE:
 			return "PROPERTY_HINT_ARRAY_TYPE"
+		PROPERTY_HINT_DICTIONARY_TYPE:
+			return "PROPERTY_HINT_DICTIONARY_TYPE"
 		PROPERTY_HINT_LOCALE_ID:
 			return "PROPERTY_HINT_LOCALE_ID"
 		PROPERTY_HINT_LOCALIZABLE_STRING:

modules/gdscript/gdscript_analyzer.cpp Show resolved Hide resolved
@@ -4371,6 +4371,14 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
export_type.type_source = variable->datatype.type_source;
}

bool is_dict = false;
Copy link
Member

Choose a reason for hiding this comment

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

Note that the is_array is used to apply export annotations to array elements, see #82952 for details. Not sure if a similar feature makes sense for dictionaries, perhaps your code needs it to simplify handling of key and value types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't speak for the other types of export annotations, but this is still needed for the basic @export scenario; otherwise, all exported dictionaries would show up as untyped in the inspector

@@ -4459,6 +4472,120 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), p_annotation);
return false;
}

if (is_dict) {
Copy link
Member

Choose a reason for hiding this comment

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

This code may be in the wrong place. if (is_array) is one level higher, after if (use_default_variable_type_check).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thiiink this is fine. This bit was tricky because I had to parse key & value independently, so there was a fair bit of repeat code as a result. It does the same logic as the outer if (is_array) check, albeit resolved earlier.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is fine. But eventually we should extract the logic to avoid the repetition and make sure it's consistent.

core/variant/variant_parser.cpp Show resolved Hide resolved
@fire

This comment was marked as resolved.

@AThousandShips
Copy link
Member

Serialisation now makes {}} in some cases

@AThousandShips
Copy link
Member

This shows we need some unit tests for this as the unit tests completely missed this, only the doc generator showed it which is an issue

@Repiteo Repiteo force-pushed the typed-dictionary branch 3 times, most recently from f6d14ff to 101e47a Compare September 3, 2024 17:58
@Repiteo Repiteo force-pushed the typed-dictionary branch 2 times, most recently from f38554a to c1a3195 Compare September 3, 2024 19:20
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.

Reviewed mostly the GDScript part and have some small notes but nothing blocking. Thanks a lot for working on this and sorry again for taking so long to review.

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

@ajreckof ajreckof left a comment

Choose a reason for hiding this comment

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

I checked the code modifications on EditorPropertyDictionary changes and tested it. This looks good.

@akien-mga akien-mga merged commit 0b4ae20 into godotengine:master Sep 6, 2024
20 checks passed
@akien-mga
Copy link
Member

Thanks! Amazing work 🎉 🥇

This is one of the most awaited features for GDScript users, and thus a major highlight for 4.4!

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.

Add static type hints for dictionary members