Skip to content

Instantly share code, notes, and snippets.

@RpxdYTX
Last active January 12, 2026 18:16
Show Gist options
  • Select an option

  • Save RpxdYTX/ad8c86191d844254fd1317e2004e0b08 to your computer and use it in GitHub Desktop.

Select an option

Save RpxdYTX/ad8c86191d844254fd1317e2004e0b08 to your computer and use it in GitHub Desktop.
Godot 4 Compile Time* Generics
class_name GenericType extends Node
## A class helper to create custom generic types through a template script
##
## Example: [codeblock]
## # prevent instances without specified types with @asbtract (very important else code might crash)
## @abstract class_name Foo extends RefCounted
##
## const T := GenericType.Param
## static func with(type): GenericType.register(Foo, { T = type })
##
## func foo(x: Foo.T) -> Foo.T:
## # duck typed, will work as long as the type argument has these functions/properties
## return T.type().some_static_function(T.type().some_static_property)
##
## # then, somewhere in the code (like in _ready or in NOTIFICATION_POST_SAVE):
##
## Foo.with(&'built in type') # ints, object, node
## Foo.with(MyCustomClassInAScript) # types defined by class_name (inner classes and enums should use the above option, with full path (aka. Foo.Bar) instead)
## [/codeblock]
##
## Remember, the code needs to be ran once before the class is registered/updated (unless when using NOTIFICATION_POST_SAVE)
## After running, use it like FooT (e.g, FooInt)
##
## This class generates the scripts on _ready
## If using NOTIFICATION_POST_SAVE, make this a [annotation @GDScript.tool] and call [method GenericType.request_ready]
## The folder where the generated scripts will be stored
const TARGET_FOLDER := 'res://generics/'
## A helper constant used to determine what is a generic type parameter
const Param := GenericType
## A helper function used to support accessing static members and functions
##
## Absolutely DO [b]NOT[/b] use it outside of template scripts
static func type(): pass
## Helper class to make monomorphizations
class Monomorphization:
var base: GDScript
var args: Dictionary[StringName, StringName]
func _init(base: GDScript, args: Dictionary[StringName, StringName]) -> void:
self.base = base
self.args = args
static var _types: Dictionary[Monomorphization, bool]
## Registers a generic type specialization (monomorphization)
## The file with said monomorphization will be created on [method GenericType._ready]
##
## Note: the class does not check for the existence of properties/methods inside the provided types
static func register(type: GDScript, args: Dictionary[StringName, Variant]) -> void:
for arg in args:
if args[arg] is GDScript: args[arg] = args[arg].get_global_name()
var params: Dictionary[StringName, StringName]
params.assign(args)
_types[Monomorphization.new(type, params)] = true
static var _abstr_rgx := RegEx.create_from_string(r'@abstract *')
static var _xtnds_rgx := RegEx.create_from_string(r'extends +(\w+)')
static var _cname_rgx := RegEx.create_from_string(r'class_name +(\w+)')
static var _param_rgx := RegEx.create_from_string(r'const +(\w+) *:= *GenericType.Param')
func _ready() -> void:
for type: Monomorphization in _types:
var type_name := type.base.get_global_name()
var gen_str := type.base.source_code
gen_str = _abstr_rgx.sub(gen_str, '')
gen_str = _param_rgx.sub(gen_str, '', true)
gen_str = _xtnds_rgx.sub(gen_str, 'extends '+type.base.get_instance_base_type())
var generics: String = type.args.values().reduce(func(a: StringName, b: StringName):
return a+b.to_pascal_case()
)
gen_str = _cname_rgx.sub(gen_str, 'class_name $1'+generics.to_pascal_case())
# this is valid, what
for match in _param_rgx.search_all(type.base.source_code):
var generic := match.strings[1]
gen_str = gen_str.replace(type_name+'.'+generic+'.type()', type.args[generic])
gen_str = gen_str.replace(type_name+'.'+generic, type.args[generic])
gen_str = gen_str.replace(generic+'.type()', type.args[generic])
var file_path := TARGET_FOLDER + _script_name(type.base, type.args)
var f := FileAccess.open(file_path, FileAccess.WRITE)
f.store_string(gen_str)
f.flush()
f.close()
load(file_path).emit_changed()
static func _script_name(type: GDScript, args: Dictionary[StringName, StringName]) -> String:
var ty := type.get_global_name().to_snake_case()
var generics: String = args.values().reduce(func(a: StringName, b: StringName):
return a + b.to_pascal_case()
)
return '%s_%s.gd'%[ty, generics.to_snake_case()]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment