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

Add ability to pass arguments to functions by name #902

Closed
PLyczkowski opened this issue May 26, 2020 · 30 comments
Closed

Add ability to pass arguments to functions by name #902

PLyczkowski opened this issue May 26, 2020 · 30 comments

Comments

@PLyczkowski
Copy link

Describe the project you are working on:

Any project.

Describe the problem or limitation you are having in your project:

Each time I add/remove function parameters, change their order, or change which are mandatory, I have to manually hunt for all function calls and update them.
Especially in setup functions that have dependency injection, mode setting etc.

Describe the feature / enhancement and how it helps to overcome the problem or limitation:

The feature allows calling parameters by name.

The future makes changing parameters less of a problem. For instance if I have function calls that use only one parameter of all available ones by name, changing their order or adding new non-mandatory ones does not break the function call.

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:

The future is already existing in python, and works like this:

func foo(val1:String = "c", val2:String = "c"):
    print(val1)
    print(val2)

foo(val2 = "a") # prints "c a"
foo(val2 = "a", val1 = "b") # prints "b a"

More detailed explanation: https://treyhunner.com/2018/04/keyword-arguments-in-python/

Here is a practical example of usage:

func build_spacefighter(
  hull:int, # Mandatory
  engines:int,
  front_gun:int = 0, # Optional
  side_guns_1:int = 0,
  side_guns_2:int = 0,
  missiles_1:int = 0,
  missiles_2:int = 0,
  torpedo:int = 0,
  targeting_system:int = 0,
  cloak:int = 0,
  shield_generator:int = 0,
  ):
  
  # Rest of function
  
var bomber = build_spacefighter(hull = Parts.HULL_A, engines = Parts.ENGINE_C, torpedo = Parts.TORPEDO_PHOTON)

Same code without this function:

var bomber = build_spacefighter(Parts.HULL_A, Parts.ENGINE_C, 0, 0, 0, 0, 0, Parts.TORPEDO_PHOTON)

^ And adding new ship parts before the called ones means changing amount of zeroes in the function call every time.

If this enhancement will not be used often, can it be worked around with a few lines of script?:

Nope.

Is there a reason why this should be core and not an add-on in the asset library?:

It's would be part of GDScript.

@PLyczkowski
Copy link
Author

Previous discussion about this proposal can be found here: godotengine/godot#6866 (comment)

@vnen
Copy link
Member

vnen commented May 26, 2020

Just to summarize the technical issues:

  1. Argument names are stripped on release.
    • This is the biggest one that limits this feature. We could add them back on release, but the increased binary size just for this feature doesn't seem worth it.
    • Additionally, if we ever add an intermediate representation to compile GDScript on export, then we could partially worked around, but the next points still apply.
  2. Functions aren't always known at compile time.
    • In fact, unless you use static typing, functions are usually not known at compile time. Like $Sprite2D.rotate(PI), uses get_node which can return any generic Node. If you don't have scene info available (which is always the case when compiling GDScript on its own) you can't tell what the rotate() function parameters are.
  3. Adding this at runtime would incur in massive performance overhead.
    • If we try to resolve this at runtime, at which point we know what the function is, then we would have to spend quite some time figuring out the proper order to pass the arguments. This would hit call performance considerably.
    • Users don't expect this to cause a performance hit, and if they did they just wouldn't use the feature, making it pointless to implement.

Potential solution:

To not be only negative, there's one thing feasible to implement:

If the function is known at compile time then I could use the compiler to de-mangle the arguments and push them in correct order.

With a few natural caveats:

  • If the function isn't known, this would throw a compiler error.
  • Using static types and casting would increase the places where you can use this.
  • The names are obtained from the found function. If you override a function and change parameter names, the named arguments are still dependent on which function is detected (so if it detects the super class, then it uses the names defined there).

I'm not sure if it's worth it adding with these restrictions. If it is, I can consider adding once the main features are working in the new GDScript.

@jonbonazza
Copy link

If the function isn't known, this would throw a compiler error.

Is there a deterministic way go know whether the function is or isn't known at compile time? If so, are the "rules" simple enough to understand at a glance? I'm skeptical here.

The names are obtained from the found function. If you override a function and change parameter names, the named arguments are still dependent on which function is detected (so if it detects the super class, then it uses the names defined there).

This would cause so much confusion.

I really don't see how this proposal is really even that useful. In fact, in languages this is supported, its use is often frowned upon because it negatively affects readability and maintainability for the average programmer.

At the very least, I don't think the bebefits here (if any even exist) outweigh the plethora of cons.

@Zireael07
Copy link

I haven't seen many criticisms of named parameters in Python or Lua, and for functions taking many parameters, their use increases readability and reduces occurrence of bugs.

@fleskesvor
Copy link

I've never heard of any criticism of named parameters in Kotlin either, and it's generally regarded as an improvement over positional parameters only in Java, where developers have to rely on the IDE to keep track of parameters. Also, no one is suggesting that named parameters replace positional parameters; only that it's offered as an alternative, like in Python, Lua and Kotlin, so there are no cons, unless performance is affected negatively.

@mnn
Copy link

mnn commented Jun 2, 2020

This would be nice. Currently I am using an optional Dictionary parameter (performance-wise - that's one allocation):

func format_time_(time: float, options: Dictionary = {}) -> String:

But that has a lot of drawbacks. First, just from a function definition, one cannot tell which options the function supports, so I have to not forget to document them (docs are not great currently):

## Format time.
## @param time {float} Time in seconds
## @param options {Dictionary}
## Options:
## * `format`           - default `TimeFormat.DEFAULT` (hours + minutes + seconds)
## * `delim`            - delimiter, default `":"`
## * `digit_format`     - default `"%02d"`
## * `hours_format`     - default is `digit_format`
## * `minutes_format`   - default is `digit_format`
## * `seconds_format`   - default is `digit_format` (or `"%05.2f"` when TimeFormat.MILISECONDS is in `format`)
...
func format_time_(time: float, options: Dictionary = {}) -> String:
...

And a second drawback, I have to extract them from the dictionary and fill in the default values (performance-wise: for every field in options that's one call to get or GG.get_fld_or_else_). Not only it's not apparent from the function signature what it expects in the options arg, default values are also not present in the signature. So a user can't simply write a name of a function followed by ( in the editor to view the function signature and see what the function supports, he has to navigate to the function definition to read the docs or read the generated docs elsewhere.

2020-06-02_21-04

Reading code is pretty good (compared to named args, it's just few characters longer):

func format_time(time: float) -> String:
    return GG.format_time_(time, {format = GG.TimeFormat.MINSEC, minutes_format = "%d"})

But using such function (writing code calling it) has, in my opinion, pretty bad UX and performance is most surely worse than anything you could come up with in the engine.

Edit: Totally forgot, names of named args could be suggested by the editor (relatively easily I think). Dictionary options would require to have first a way to describe the type (shape) of it (it's not currently possible) and then suggest fields of the options object based on its type (I believe that will be much harder to implement compared to named args suggestions).

@PLyczkowski
Copy link
Author

This would be nice. Currently I am using an optional Dictionary parameter (performance-wise - that's one allocation)

Another drawback is that these arguments don't get parsed, so you get failures at runtime.

@Error7Studios
Copy link

@vnen
(Preface: I have no clue how function argument suggestions works in C++, so forgive my ignorance.)
But could something like this potentially work to incorporate named args?

Assume there's a singleton class called FuncArg which stores data from all function signatures.
Function calls using named args are converted into a function call in FuncArg.
How it would do this, I don't know.

Or if that would kill performance, maybe when running the game, if a script uses function calls with named args, a new (duplicate) script is created, and the func call is replaced in-line (like a C++ macro) with a func call with ordered default/named arguments.
Then the new script replaces the original one.
(But these are probably really stupid ideas.)

Example Project: FuncArg.zip

Player.gd

extends Sprite

var thing
var health := 100
var alias := "Character"
var color := Color.gray

func set_vars(__thing, __health := 250, __alias := "Wizard", __color := Color.blue) -> void:
	thing = __thing
	health = __health
	alias = __alias
	color = __color

func print_vars() -> void:
	for __var_name in ["thing", "health", "alias", "color"]:
		print(__var_name, ": ", get(__var_name))

func _ready() -> void:
	print_vars()
	print("-----")
	
	# should be called by: set_vars(__color = Color.red, __thing = Image.new())
	FuncArg.call_using_named_args(self, "set_vars", {"__color": Color.red, "__thing": Image.new()})
	
	print("Default args for set_vars(): ", FuncArg.get_arg_defaults(self, "set_vars"))
	print("-----")
	
	print_vars()

FuncArg.call_using_named_args(self, "set_vars", {"__color": Color.red, "__thing": Image.new()})
should be called automatically when calling: set_vars(__color = Color.red, __thing = Image.new())

The output is:

thing: Null
health: 100
alias: Character
color: 0.75,0.75,0.75,1
-----
Default args for set_vars(): [(NO_DEFAULT), 250, Wizard, 0,0,1,1]
-----
thing: [Image:1210]
health: 250
alias: Wizard
color: 1,0,0,1

FuncArg singleton:

extends Node

const NO_DEFAULT := "(NO_DEFAULT)"
const NO_TYPE := "(NO_TYPE)"

# after saving 'Player.gd', 'data' should be:
var data := {
	"res://Scenes/Player/Player.gd": {
		"set_vars": {
			"__thing": {"default": NO_DEFAULT, "type_name": NO_TYPE},
			"__health": {"default": 250, "type_name": "int"},
			"__alias": {"default": "Wizard", "type_name": "String"},
			"__color": {"default": Color.blue, "type_name": "Color"},
		}
	},
}

func get_func_dict(__object: Object, __func_name: String) -> Dictionary:
	if !__object:
		push_error("Can't call on null object")
		return {}
	if !__object.has_method(__func_name):
		push_error(str("No method named '", __func_name, "' in object: ", __object))
		return {}
	
	var __script: Script = __object.get_script()
	if !__script:
		push_error(str("No script attached to object: ", __object))
		return {}
	
	var __script_path: String = __script.resource_path
	if !data.has(__script_path):
		push_error(str("Invalid script path:", __script_path))
		return {}
	else:
		return data[__script_path]

func get_arg_dict(__object: Object, __func_name: String) -> Dictionary:
	var __func_dict := get_func_dict(__object, __func_name)
	if __func_dict.empty():
		return {}
	elif !__func_name in __func_dict:
		push_error(str("Invalid func name:", __func_name))
		return {}
	else:
		return __func_dict[__func_name]

func get_arg_names(__object: Object, __func_name: String) -> Array:
	var __arg_dict: Dictionary = get_arg_dict(__object, __func_name)
	if __arg_dict.empty():
		return []
	else:
		return __arg_dict.keys()

func get_arg_defaults(__object: Object, __func_name: String) -> Array:
	var __arg_dict: Dictionary = get_arg_dict(__object, __func_name)
	var __arg_defaults := []
	for __subdict in __arg_dict.values():
		__arg_defaults.append(__subdict["default"])
	return __arg_defaults

func get_arg_default(__object: Object, __func_name: String, __arg_name: String):
	var __arg_names: Array = get_arg_names(__object, __func_name)
	var __arg_defaults := get_arg_defaults(__object, __func_name)
	if !__arg_name in __arg_names:
		push_error(str("Invalid argument name: ", __arg_name))
		return
	else:
		return __arg_defaults[__arg_names.find(__arg_name)]

func call_using_named_args(__object: Object, __func_name: String, __passed_arg_data: Dictionary):
	var __arg_names := get_arg_names(__object, __func_name)
	var __arg_defaults := get_arg_defaults(__object, __func_name)
	var __passed_arg_names := __passed_arg_data.keys()
	var __passed_arg_values := __passed_arg_data.values()
	
	for __passed_arg_name in __passed_arg_names:
		if !__passed_arg_name in __arg_names:
			push_error(str("Invalid argument name: ", __passed_arg_name, ". Can't call '", __func_name, "'"))
			return

	var __ordered_args := []

	for __i in __arg_names.size():
		var __arg_name: String = __arg_names[__i]
		var __arg_default = __arg_defaults[__i]
		
		var __required_argument_supplied := true
		
		if str(__arg_default) == NO_DEFAULT:
			if !__passed_arg_names.has(__arg_name):
				push_error(str("Value not supplied for argument '", __arg_name, "'"))
				__required_argument_supplied = false
		
		if __required_argument_supplied:
			if __passed_arg_names.has(__arg_name):
				__ordered_args.append(__passed_arg_data[__arg_name])
			else:
				__ordered_args.append(__arg_defaults[__i])
	
	if __ordered_args.size() == __arg_names.size():
		return __object.callv(__func_name, __ordered_args)

@ghost
Copy link

ghost commented Sep 30, 2021

I would say benefits of named arguments outway the risks/performance hit. Would help make the code more readable and easier to understand and reduce incidence of bugs. This also would fall in line with all the other Godot 4 changes aimed to make the engine more accessible and easier to use and understand (such as class/member renames, 2D/3D node renames, removing && || etc).

@Calinou Calinou changed the title Ability to pass arguments to functions by name Add ability to pass arguments to functions by name Sep 30, 2021
@vnen
Copy link
Member

vnen commented Oct 4, 2021

@Error7Studios the problem is that we don't always know the actual function at compile time, so anything like that would have to happen at runtime (and every time the function is called). The other issue is that release builds strip the argument names, meaning it would be impossible to tell the order of the arguments.

@filipworksdev assuming we would add argument names on release builds (which would increase the binary size), the performance hit is very likely not worthy for this feature, because it would make function calls much slower than they are now. In many cases this wouldn't be noticeable, but when calling things inside a loop this can become a problem very easily. While we value usability over performance, in this case I believe it's not worth the trade-off.

@jtrack3d
Copy link

Visual Studio for C# now has an option where the editor shows the parameter names inline as you type or review the file while they aren't really there. Not sure how they do it, but then it doesn't really change the file, it's just a rendering in the editor concern rather than interpreter concern. I think it is called inline parameter hints and is optional.

@Razzlegames
Copy link

@jtrack3d I agree, that's handy and it would be great if we could get this feature in the editor. But named parameters are much more than that as language feature, in that they allow arbitrary ordering of parameters and explicit parameter assignment, where the code is much more self documenting.

For me, the easy work around for now is:

  1. group parameters in to data transfer classes for method calls with many parameters
  2. Or use a dictionary for named parameters (slight performance hit on hash table lookup)
  3. Just refactor your code into simpler functions.

I only use option 1 and 3 so far

@nathanfranke
Copy link

Physics2DDirectSpaceState::intersect_point takes a ton of parameters, and I usually only need to modify a few.

Array intersect_point(point: Vector2, max_results: int = 32, exclude: Array = [  ], collision_layer: int = 0x7FFFFFFF, collide_with_bodies: bool = true, collide_with_areas: bool = false)

For example right now I need to only intersect with areas, so I swap the last two boolean parameters:

state.intersect_point(event.position, 32, [], 0x7FFFFFFF, false, true)

Of course that adds a lot of copies of these parameters, and it's not clear which ones are default and which ones are changed.


Regarding the technical issues, I'd imagine these parameters should be resolved during "compile time" (when .gd is converted to .gdc). Not sure if that even happens when debugging though, if not maybe the functionality will need to be done twice (Once in the compilation step, once in the interpreting/debugging step, if it even works that way).

@FredericoCoelhoNunes
Copy link

Any update on this? It makes the code difficult to read having to count the arguments in the function signature every time I am looking for an argument in particular (especially for functions with several arguments). It looks like a small thing but after a while it definitely starts becoming noticeable.

@BanchouBoo
Copy link

If the technical problems in implementing this are too much to be worth contending with, perhaps an alternative could be to have a default keyword you can pass into a function argument which will just load the default value for that particular argument.

Not a perfect solution to what named arguments solve as you still have to worry about updating code when you add or rearrange arguments, but it removes one level of overhead in manually passing in the default value so you don't have to track it in a variable or hardcode it to match and manually update it everywhere if the default value changes

@vnen
Copy link
Member

vnen commented Jan 16, 2023

Any update on this?

There isn't anything to update, the technical roadblocks I mentioned are still in place and I see no way of overcoming them. I'm not particularly looking for solutions so it'll probably stay like it is. If anyone has a solution in mind I would like to know.

In general: There's no way to implement this that is complete and don't affect runtime performance.

If the technical problems in implementing this are too much to be worth contending with, perhaps an alternative could be to have a default keyword you can pass into a function argument which will just load the default value for that particular argument.

I can see some other issues with this, but it's better to be discussed as its own proposal. It is more feasible than this one though.

@vnen
Copy link
Member

vnen commented Jan 16, 2023

Given the current technical limitations, there's simply no feasible way to implement this, so I'm finally closing the proposal. If there's a valid implementation solution we can reassess and reopen (or such implementation project could be sent as a new proposal).

@vnen vnen closed this as not planned Won't fix, can't repro, duplicate, stale Jan 16, 2023
@vnen vnen moved this from In Discussion to Not Planned in Godot Proposal Metaverse Jan 16, 2023
@vonagam
Copy link

vonagam commented Jan 16, 2023

Not really invested, but to check that I understand correctly: there is a way to make it work at compile time without runtime penalties and the condition for that is that the code should be typed (at least at the place of calling). Why is it considered to be a blocker? Seems like a reasonable requirement and one that adds motivation to use types.

@lyuma
Copy link

lyuma commented Jan 18, 2023

@vonagam Since you're making the case for when the function signature is known in compile time due to strong typing... in that case, it seems to me that the IDE could be modified to show function arguments in the way modern IDEs do, such as how this plugin does it:
https://marketplace.visualstudio.com/items?itemName=liamhammett.inline-parameters

image

Seems to me the same could be done for the built-in script editor, too... and since it's editor time there's no worry about runtime script performance overhead

from the perspective of a programmer, what is the difference if the parameter names shown by the IDE, versus if you type them yourself?

I imagine that a proposal for improving the script editor in this way could be accepted.

@vonagam
Copy link

vonagam commented Jan 18, 2023

The proposal isn't about showing names for ordered parameters. It is about ability to pass in any order and skip some default ones. Annotation usage of passing arguments by names is secondary one.

In all examples and snippets here people use typed code and it was told that there is no problem to do it at compile time if the code is typed. So I'm trying to understand why was the proposal closed.

@vnen
Copy link
Member

vnen commented Feb 23, 2023

@vonagam there is still the problem that argument names are stripped in release builds, so even with everything typed it couldn't work, at least not for native classes.

@Pixdigit
Copy link

I know this is just a very tangential solution, but it would be easy to implement.
When calling a function with an argument _ it would be replaced by the default value for that parameter. If at runtime there is no default value use either null for nullable types or throw an error. Example:

func i_have_many_arguments(first, second : Node, third=3, fourth="four", fifth="five"):
    print(first)
    print(second)
    print(third)
    print(fourth)
    print(fifth)

i_have_many_arguments("1", _, _, "4")

would print

1
null
3
4
five

@ridilculous
Copy link

  • Argument names are stripped on release.

    • This is the biggest one that limits this feature. We could add them back on release, but the increased binary size just for this feature doesn't seem worth it.
    • Additionally, if we ever add an intermediate representation to compile GDScript on export, then we could partially worked around, but the next points still apply.

Sorry if that's too naive but when argument names are stripped, couldn't named parameters be transformed into positional parameters?

@Walter-o
Copy link

Requesting a second look at this since keyword arguments are a barebones essential for clean code.

Throwing a compiler error on messing up the order of keyword arguments or mixing would be an insanely good compromise for getting this feature in.

Like take this function for example:

func test(x, y):
    ...

These calls would be legal:

test(1, 2)
test(x=1, y=2)

These calls would be illegal

test(y=2, x=1)
test(x=1, 2)
test(y=2, 1)
  • Just verify all kwargs match the parameter names and no kwargs/args mixing is happening
  • Raise compiler error if its bad
  • If all is good, ignore the keywords and treat the function like normal

@dalexeev
Copy link
Member

@Walter-o This would only work if the function is known at compile time. If the argument list is not known in advance, this cannot be solved without a runtime penalty:

untyped_var.test(x=1, y=2)
$Node.test(x=1, y=2)

@Walter-o
Copy link

@dalexeev Ow that's really difficult then.
I'm always used to programming in environments where everything is already resolved.

@Ivorforce
Copy link

Ivorforce commented Oct 8, 2024

In an attempt to get this re-opened, I want to propose a feasible and reasonable way to implement it.

I believe in the importance of named arguments for clean and understandable code. I think closing a proposal as important as this one because of implementation feasability is a mistake, especially since many proposals are already lying around, waiting possibly for years until they can finally be picked up with a non-disruptive implementation. But yes, I do have an implementation proposal, too.

non kwargs call

Regular calls stay as-is (without a kwargs overhead).

kwargs call

kwargs using calls produce an additional arg_names list of StringName in the GDScript VM bytecode (not during runtime). Matching the names to the interface has some runtime cost for kwargs using functions, but may be near negligible with a good implementation, e.g. (pseudocode):

def map_args_with_kwargs(interface, call_args, call_arg_names):
  args = list(interface.params.size())
  non_kwargs_size = args.size() - call_arg_names.size()

  # Copy over normal args
  args[:non_kwargs_size] = call_args[:non_kwargs_size]
  args_mask = [True] * interface.params.size()

  for name, value in zip(call_arg_names , args[non_kwargs_size:]):
      idx = interface.params.get_idx(name)
      if idx < 0:
          raise "Unknown parameter " + name
      if args_mask[idx]:
          raise "Multiple values for key " + name
      args_mask[idx] = true
      args[idx] = value

  for idx in range(interface.params.size() - args.size(), None):
    if args_mask[idx]:
      continue
    if !params.has_default_arg[idx]:
      raise "Too few arguments for call"

    args[i] = params.default_arg[idx]

  return args

Static analysis optimization

Unlike python, the godot type annotation system ensures that variant types are known. Known-type kwargs calls can therefore be optimized at build time, as previously suggested:

if kwargs.empty()
    produce_non_kwargs_call(args)
if type_annotation is not None:
    args = map_args(args, kwargs)
    produce_non_kwargs_call(args)
else:
   produce_kwargs_call(args, kwargs)

Given people interested in kwargs probably have a large overlap with those concerned about type safety, this should cover most use-cases.

Argument Name Stripping

Above, concerns were raised because argument names are currently stripped for release builds. There are 3 ways to go about this (in order of implementation complexity):

  1. Fail on runtime kwargs calls. I don't like this one, but it can still be used if argument names absolutely must be stripped.
  2. Don't strip argument names. That's what python does, and it's not a huge concern space wise
  3. Instead of just stripping argument names, assign each known StringName a global int ID and include that in the distributable. With this, argument names are obfuscated and still unknown at runtime.
    • These could also be stored in the bytecode directly, further improving runtime speed of these calls.

Conclusion

Runtime costs of regular non-kwargs calls are unaffected.
Runtime costs of kwargs calls to known types are the same as non-kwargs calls.
Runtime costs of runtime kwargs calls are acceptable (fast implementation without runtime hashes, thanks to StringName), or simply forbidden.

I hope this write-up illustrates that kwargs are indeed not only possible for GDScript, but also low-risk as well as reasonable to implement.

@dalexeev
Copy link
Member

dalexeev commented Oct 9, 2024

@Ivorforce Note that there is no single API for calling functions. GDScript functions are called in one way, utility functions in second way, native functions in third way (in fact there are even more). Also, almost any method/function can be represented as a Callable, which has a vararg method call(). The question arises whether vararg methods in general and Callable.call() in particular should support named arguments. I don't think it should, although many users would assume it should, I suppose.

So we can't solve this only for GDScript, it should also work properly for core, C# and GDExtension. Besides "don't strip argument names" we should make sure that all APIs that can be invoked in GDScript support named arguments. At least through "reflection" (MethodInfo), so that we can determine the order of parameters from their names at runtime.

Regarding performance, note that in GDScript default arguments can be non-constant expressions, unlike in some other languages. The ability to pass arguments by name means that you can skip some optional arguments. This means that we can't simply patch the passed argument array. Instead, we have to replace the jump to the first non-specified default argument (or function body) with a loop or a check chain. This may not be a big deal, but it still affects all functions with optional arguments, regardless of whether you use named arguments or not, and whether the method and its arguments are known at compile time or not.

@Ivorforce
Copy link

Ivorforce commented Oct 9, 2024

Thank you for the considerations. I have some answers to your comments:

The question arises whether vararg methods in general and Callable.call() in particular should support named arguments.

The answer, in this proposal, is of course "yes". If kwargs should be supported, they should be supported everywhere. Non-kwargs calls can be kept around as an 'optimization' (not having to supply an empty kwargs dict). Having various endpoints of how calls work certainly makes it a bit more difficult, but through a unified understanding of MethodInfo, the logic should be unifiable in a single, simple spot - after the abstraction of kwargs calls in Callable, the receiver should not need to know that both kwargs and non-kwargs calls can be made. That is my (admittedly optimistic) guess without having seen the relevant source code.

(Edit: Though an argument could be made that kwargs calls could still be very helpful even as a compile time only feature, converted to normal args calls by the GDScript interpreter, for type annotated calls)

Besides "don't strip argument names" we should make sure that all APIs that can be invoked in GDScript support named arguments.

Through argument names, they already do! As you pointed out yourself, existing argument names are enough to figure out how to map *args, **kwargs calls into a simple args call, evaluated at registration. Receivers shouldn't even need to be modified, this is purely Callable boilerplate.

Regarding performance, note that in GDScript default arguments can be non-constant expressions [...]

This is certainly an interesting sub-problem. After some thinking I believe I have a decent response for that, inspired by ROP jumps.

I assume the compiler is currently generating VM code for default parameters, and skipping past the ones supplied by entering into the function later. The code would have to be modified such that after each argument initialization, a jump is performed to the next uninitialized argument.

Basically, the GDScript gdextension loads on the stack the entrypoints for each of the missing parameters. The function is then called at the first entrypoint, upon which the initializers are jumped through depending on which are needed for initialization.
It's a bit like an if param_initialized[idx]: ..., but a bit faster since the corresponding boolean is already present on the stack. This could of course be a simpler alternative implementation, simply defining a mask somewhere that is checked for which parameters still need initialization.

The current entrypoint can be kept as-is, essentially generating the parameter intializers twice - once for non kwargs calls with simple entry jumps, and once for kwargs calls with ROP-like consecutive jumps into needed default fillers.

@dalexeev
Copy link
Member

dalexeev commented Oct 9, 2024

The answer, in this proposal, is of course "yes". If kwargs should be supported, they should be supported everywhere.

So you're advocating the Python way? I was only thinking of something like JavaScript and PHP: you can pass existing arguments by name, and you can pass extra arguments (for variadic functions) packed into an array. But you can't pass non-existent arguments by name, that would be a compile-time or runtime error.

Godot has functions with a v suffix, like call() and callv(). callv() passes argument values ​​as an array, there's no room for names. What you're proposing involves introducing a new method like calld() that takes a dictionary1.

Given the above, I think what you proposed can still be quite complex and is something more than what we discussed above (since your proposal affects not only GDScript, but also core, C#, GDExtension).

I think closing a proposal as important as this one because of implementation feasability is a mistake [...]

Well, I think we are sometimes inconsistent about complex proposals. This particular one we closed more because we have no hope of implementing it, not because we don't want to have the feature. While there are other popular proposals that are still open, but frankly have little chance of being implemented in the foreseeable future due to the complexity and amount of refactoring required. It is difficult to measure hope objectively.

Maybe we should re-open this proposal, although this still does not mean that we plan to implement it. Or maybe we should close/archive other proposals that have too little chance of being implemented in the foreseeable future. Or use a GitHub project, labels or milestones to categorize proposals by their achievability.

But I think you could open a new proposal with new technical information if you think it is possible to address all the concerns. In my opinion, what you are proposing goes beyond the original idea.

Footnotes

  1. Positional and named arguments can be mixed, but we could represent this with integer keys. This would be calld(args: Dictionary[int|String, Variant]) if we had nested and union types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Not Planned
Development

No branches or pull requests