Skip to content

Static Variable Initialization Order Issue in GDScript #104161

Open
@luckyabsoluter

Description

@luckyabsoluter

Tested versions

v4.4.stable.official [4c311cb]

System information

Godot v4.4.stable

Issue description

Description:
When defining a static variable that depends on another static variable from a nested class, GDScript fails to initialize it in the expected order, resulting in null instead of the intended value.

Steps to Reproduce:

extends Node

class MyClass :
    static var i_will_be_one = 1

static var am_i_one = MyClass.i_will_be_one

func _ready():
    print("am_i_one: ", am_i_one)  # Expected: 1, Actual: <null>

Expected Behavior:

  • am_i_one should correctly be initialized to 1.

Actual Behavior:

  • am_i_one is <null> because MyClass.i_will_be_one is not initialized before am_i_one is assigned.

Analysis:
This issue seems to be related to static variable initialization order. In some other languages like Java, similar issues arise when circular dependencies exist between static initializations. Java solves this by ensuring that static fields are initialized in a intelligent order.

Java Example of a Similar Issue:

class A {
    static int i = B.j; // Depends on B.j
}

class B {
    static int j = A.i; // Depends on A.i
}

public class Main {
    public static void main(String[] args) {
        System.out.println("A.i: " + A.i); // 0
        System.out.println("B.j: " + B.j); // 0
    }
}

Explanation:

  • A.i depends on B.j, and B.j depends on A.i, causing a circular dependency.
  • Java detects this and assigns both values their default (0 for int).
  • However, if either A.i or B.j were explicitly set to 1, Java would propagate that value correctly instead of defaulting to 0, demonstrating its ability to intelligently resolve dependencies when possible.

Java Example with Explicit Initialization:

class A {
    static int i = 1; // Explicitly set to 1
}

class B {
    static int j = A.i; // Depends on A.i
}

public class Main {
    public static void main(String[] args) {
        System.out.println("A.i: " + A.i); // 1
        System.out.println("B.j: " + B.j); // 1
    }
}

Explanation:

  • Since A.i is explicitly initialized to 1, Java correctly resolves B.j = A.i to 1 as well.
  • This shows that Java does not simply assign default values arbitrarily but instead attempts to propagate known values when possible.

Another Java Example with Reversed Dependency:

class A {
    static int i = B.j; // Depends on B.j
}

class B {
    static int j = 1; // Explicitly set to 1
}

public class Main {
    public static void main(String[] args) {
        System.out.println("A.i: " + A.i); // 1
        System.out.println("B.j: " + B.j); // 1
    }
}

Explanation:

  • Since B.j is explicitly initialized first, Java correctly resolves A.i = B.j to 1.
  • This further demonstrates Java’s ability to intelligently propagate initialization when possible.

Java's Solution:

Java prevents this issue by enforcing a well-defined initialization order:

  1. If one static field directly depends on another, Java ensures that the dependent class is loaded first.
  2. If circular dependencies exist, Java assigns default values instead of leaving them uninitialized.
  3. If at least one of the dependent variables is explicitly initialized, Java correctly resolves and propagates that value.
  4. Developers can avoid potential order issues by restructuring dependencies or using lazy initialization.

GDScript's Limitation:

Unlike Java, GDScript lacks static initialization blocks and mechanisms for deterministic static initialization ordering. Additionally, other issues like:

static var am_i_one_too = am_i_one  # null
static var am_i_one = 1

show that even within a single class, the order of static variable initialization can cause unexpected null values, making it difficult to rely on static variables safely.

Possible Solutions:

  1. Ensure that static variables are initialized in a deterministic order, where parent class static variables are initialized after inner class static variables.
  2. Introduce a lazy initialization mechanism to defer value retrieval until all related static variables are set.
  3. Provide a warning or error message when an uninitialized static variable is accessed during initialization.

Impact:
This issue can lead to unexpected behavior in complex scripts where static variables are used for configuration or constants shared across classes.

Workaround:
Manually assign the value in _ready() instead of relying on static initialization.

Steps to reproduce

extends Node

class MyClass :
    static var i_will_be_one = 1

static var am_i_one = MyClass.i_will_be_one

func _ready():
    print("am_i_one: ", am_i_one)  # Expected: 1, Actual: <null>

Minimal reproduction project (MRP)

N/A

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status

      For team assessment

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions