Skip to content

Typed Dictionary<K, V>#1502

Merged
Bromeon merged 2 commits intomasterfrom
feature/typed-dictionary
Feb 16, 2026
Merged

Typed Dictionary<K, V>#1502
Bromeon merged 2 commits intomasterfrom
feature/typed-dictionary

Conversation

@Bromeon
Copy link
Member

@Bromeon Bromeon commented Feb 15, 2026

Closes #1450.

API changes

Added:

  • Dictionary<K, V> struct
  • AnyDictionary struct
  • Full integration with property and #[export]

Repurposed:

  • VarDictionary is now an alias for Dictionary<Variant, Variant>
  • dict! macro now creates typed dictionaries

Unfortunately, Godot's engine APIs almost never use typed dictionaries, and we cannot soundly assume dictionaries to be typed even in cases where we know all keys to be strings, for example. It's always possible to share a dictionary with GDScript code (or in Rust do a Dict<K,V> -> Variant -> VarDict roundtrip) to insert non-K/V elements.

Also, for Rust-only game logic code, there's almost never a reason to use Dictionary; HashMap or one of the many crates are faster and more idiomatic to use.

Typed dictionaries can however be useful when interfacing typed GDScript code, or when exporting properties and limiting the types that can be set from the editor. I also hope Godot's own APIs will evolve over time.

Migration

Warning

TLDR: Elements with ToGodot::Pass != ByValue now need to be passed by reference:

dict.set("hello", gd);
// new:
dict.set("hello", &gd);

I tried to retain backwards compatibility as much as possible. Unbeknownst to me, this came at massive implementation cost 😬

The TLDR is that we previously had impl ToGodot arguments that allowed dict.set("key", 123). With the move to impl AsArg<T>, untyped dicts would however have dict.set("key".to_variant(), 123.to_variant()).
Arrays already use that approach, and I thought a lot about this.

I concluded this syntax is unacceptable, for multiple reasons.

  • First, it breaks every single usage of dictionary, which godot-rust so far has only supported in untyped form. Sometimes inevitable or justifiable with new features, I usually strive to limit the breakage or at least provide smooth migration periods.
  • Second, Godot's current API situation is that untyped dictionaries are still very much needed, and going to be needed for quite a while.
  • Unlike arrays, dictionaries are very often heterogenous at least in value types; Godot regularly misappropriates them as poor man's structs.

So, decent UX would still be nice. I experimented with different approaches:

  1. Implementing AsArg<Variant> for T to enable implicit conversions.
    • This breaks type inference in other places like array![...], because i32 is now convertible to either AsArg<i32> or AsArg<Variant>. While such ambiguity already exists for strings and arrays, I'm not sure if that's a good change, especially as int array literals are very prevalent in our own tests.
  2. Implementing AsVArg<T> as a trait that supports AsArg<T> plus conversion to Variant.
    • Basically like extending AsArg itself, but limiting these variant conversions to dictionary (for now).
    • This turned out to be an absolute nightmare because of Rust's coherence rules: I can only blanket-impl AsArg<Variant> for either Pass=ByValue or Pass=ByRef types, a known limitation in the Rust compiler. Individual impls aren't great because it won't support user-defined types like enums (which should be supported in the future).
    • For a while I went with a manual approach based on macros that implement both AsArg + AsVArg at the same time. To learn that a lot of Rust's native syntax like < > isn't readily usable in declarative macros without creating ambiguity, causing weird DSLs.
  3. AsVArg<T> but using a helper trait trick.
    • Essentially, this promotes Pass from associated type to generic parameter, thus enabling rustc's trait solver to detect disjointness and allowing non-overlapping impls.
    • This took me quite a while to find out and make work. I don't like it but it's the least worst I could find. We now need an extra DisjointVArg trait, which is supposed to be hidden but might leak into user code (at least in errors) -- there's probably UX to be done on this front, not today.

In essence the implementation is a classic Rust trait hell, thanks to coherence being half-implemented in rustc. But it works and I think the resulting API and backwards-compatibility for at least some element types is worth it.

Adjustments are necessary for types that don't implement ToGodot<Pass=ByValue>, for example callables (ByRef), objects (ByObject), etc. It's usually solved by prepending a &. You shouldn't need to write .to_variant() except for maybe very exotic user-defined types.

Future work

There are a few more things to do, e.g.

  • Support enums in arrays, with correct export hints #353
  • Rename ArrayElement -> Element
  • Optimizations of some methods that can now benefit from nil being uninhabitable, thus able to call a single get_or_nil internally.
  • Make sure that engine APIs use appropriate types everywhere (&AnyDictionary or &Dictionary<K, V>).
  • Typed iterators.
  • Consider role of Dictionary::get_or_nil -- only available for V=Variant now, but equivalent to get().unwrap_or_default(). Useful in some cases (see tests).
  • Consider dict! syntax change: { key: value } to { key => value } (less ambiguity, no parentheses)
  • Re-evaluate covariance of script classes (currently representable in Gd<...> elements)

@Bromeon Bromeon added feature Adds functionality to the library c: core Core components breaking-change Requires SemVer bump hard Opposite of "good first issue": needs deeper know-how and significant design work. labels Feb 15, 2026
@Bromeon Bromeon mentioned this pull request Feb 15, 2026
@Bromeon Bromeon force-pushed the feature/typed-dictionary branch 3 times, most recently from d981ea6 to 11bff51 Compare February 15, 2026 22:47
@Bromeon Bromeon added this to the 0.5 milestone Feb 15, 2026
@Bromeon Bromeon force-pushed the feature/typed-dictionary branch from 11bff51 to eb042f8 Compare February 16, 2026 16:41
Provide extra traits `AsVArg` and `DisjointVArg` (internal) to allow
`T` -> `Variant` conversions for APIs like `set("str", 123)`.
@Bromeon Bromeon force-pushed the feature/typed-dictionary branch from eb042f8 to 8d6b4f2 Compare February 16, 2026 17:33
@Bromeon Bromeon added this pull request to the merge queue Feb 16, 2026
Merged via the queue into master with commit caa0434 Feb 16, 2026
23 checks passed
@Bromeon Bromeon deleted the feature/typed-dictionary branch February 16, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Requires SemVer bump c: core Core components feature Adds functionality to the library hard Opposite of "good first issue": needs deeper know-how and significant design work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typed Dictionary<K, V>

1 participant