GDScript progress report: Feature-complete for 4.0
By: George Marques 2 June 2021
It has been a while since my last report. In my last post, I've mentioned GDNative, but since I've done only a bunch of "boring" stuff, I ended up not writing anything about it. Work on GDNative is not completed yet, so maybe I'll have something interesting to report later on. For now, let me show what was done on GDScript.
See other articles in this Godot 4.0 GDScript series:
- GDScript progress report: Writing a tokenizer
- GDScript progress report: Writing a new parser
- GDScript progress report: Type checking is back
- GDScript progress report: New GDScript is now merged
- GDScript progress report: Typed instructions
- (you are here) GDScript progress report: Feature-complete for 4.0
One of the most requested features of GDScript is now implemented: typed arrays. Now you can set the element type and let it be validated by the engine. That allows you to do safe operations with homogeneous arrays and also allow the language to optimize a few more code paths.
The syntax is inspired by Python, like most of the type system:
var my_array: Array[int] = [1, 2, 3]
This allows you to easily change a regular array to typed and vice-versa without changing much code. You can also rely on type inference. If the array has elements of the same type, it will be inferred as typed:
var inferred_array := [1, 2, 3] # This is Array[int].
Note that types that can't be validated at compile-time (when they are dynamic) will be validated at runtime, which might impact performance slightly if done regularly.
Improving the functional side of GDScript, now you can use lambda functions (or, more specifically, function literals). Those are inline functions that are assigned directly to an expression instead of being defined in the class. This is great for functions used only in a small context – like sorting an array or connecting to a signal – without polluting the class scope with those.
The syntax is pretty much the same as a regular function, only that it is placed where an expression should be:
func _ready(): var my_lambda = func(x): print(x) my_lambda.call("hello")
Note that the
.call() is needed because lambdas are of type Callable.
You can also pass them as function arguments:
func _ready(): button_down.connect(func(): print("button was pressed"))
Lambdas can optionally have a name, which is used when viewing stack traces. They are also not limited to a single statement:
func _ready(): var my_lambda = func this_is_lambda(x): print("Hello") print("This is %s" % x)
Last but not least, lambdas can capture variables from the scope they are defined in. This means you can use variables from the outer function or the outer class inside the lambda. Those will be copied when the lambda is created at runtime and passed to function when it's called:
func _ready(): var x = 42 var my_lambda = func(): print(x) my_lambda.call() # Prints "42".
The documentation will be updated to explain this in detail in the future.
Static methods in built-in types
This is quite interesting because many times, we had to construct a value just to call a function on it. Now, the function can be called directly:
var x = Color.html_is_valid("00ffff") # true
Note that in 3.x, you already can use this syntax for some cases (such as
Color.from_hsv()) but it only works with
const functions that return the same type as the base and that is actually constructing a value at runtime. In 4.0, this won't be valid anymore for these cases, but we can have actual static functions.
I've implemented a couple of things that will help improving GDScript performance (not directly, but we still plan to work on the VM performance before 4.0). Those are internal changes that won't affect the way you code, but I'll mention them here for completeness.
One of those is reducing the number of addressing modes, which will help us eventually have instructions with addressing modes baked in. This will reduce branching in the code which is something CPUs enjoy. Having less modes means we can have less permutations of each instruction, reducing the complexity and code size.
The other is using a special stack space for typed temporaries. This means that results of sub-expressions that are typed can be stored on the stack in a Variant without changing its type nor re-initializing it. This is particularly helpful on types that require memory allocation, such as Transform, since the allocation can be done only once per call.
To improve the quality of the GDScript code and avoid regressions over time, I've added a test runner that verifies if the language code is performing as intended. I've had some help from Andrii Doroshenko who ported my suite to use the new doctest integration, effectively making the tests for GDScript part of the general test for the engine.
We don't have a lot of test cases for now, but the number of tests will increase over time as tests are added together with bug fixes to avoid regressions.
I've now shifted the focus to bringing GDScript to a more usable state even before 4.0-alpha, so more users can test it to verify how it's working. For now, my focus are in the game-breaking bugs that stops people from even using GDScript. The small things can wait a bit longer to when we reach the bugfixing round after alpha is released.