TweenInfo.new() with any arguments explicitely nil raises an error instead of using default arguments

For most constructors with optional arguments such as Vector3.new(), if you put nil for one of the arguments, they will just substitute the default value for the nil argument regardless the order.

However, with TweenInfo.new(), you do not have to provide any/all arguments, as the remaining ones will be filled with default values, but if you manually put nil for them, for example, because you want a default value for EasingStyle but not EasingDirection without explicitly typing the default value, an error is raised, instead of using the default value, even though according to the documentation, the types for the arguments are optional.

Expected behavior

Examples:

  • TweenInfo.new(nil) should be equivalent to TweenInfo.new() but raises an error instead.
  • TweenInfo.new(1, nil, Enum.EasingDirection.InOut) should be equivalent to TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) but again, raises an error instead.
1 Like

Not providing value is not the same as providing nil value.
I saw somewhere similar bug report, but I wonā€™t be able to find what engeneer said aside from what I remembered and said above.

Well, given that the types of the arguments in the documentation are optional, not providing a value should be the same as providing nil value for constructors that support optional arguments.

The types are just simplified. It really should be (number) -> TweenInfo & (number, Enum.EasingStyle) -> TweenInfo & etc, but making them optional makes it more readable.

If you really want, you should make a feature request to either change the behavior or change the type.

Technically, nil is stored as a value internally. It just happens that nil represents the absence of a value in Lua.

I havenā€™t looked at the source code of Luau, but, I believe this ā€œissueā€ stems from how default parameters work in C++.

In C++, you must pass all required arguments to a function when you want to use it:

void whatever(int a, int b, int c = 10) {}

whatever(1, 2, 3); // okay
whatever(1, 2); // okay
whatever(1); // not okay

Letā€™s assume the constructor of TweenInfo is declared as:

TweenInfo new(
    float time = 1,
    EasingStyle easingStyle = Linear,
    EasingDirection easingDirection = Out,
) // not the entire thing, but you'll get the point

Now, if we provide the function with non nil values:

// Lua: TweenInfo.new(1, Enum.EasingStyle.Sine)
new(1) // okay

// Lua: TweenInfo.new(2, Enum.EasingStyle.Sine)
new(2, Sine) // okay

// Lua: TweenInfo.new(3, Enum.EasingStyle.Sine, Enum.EasingDirection.In)
new(3, Sine, In) // okay

But if we provide a nil value:

// Lua: TweenInfo.new(1, nil, Enum.EasingStyle.In)
new(1, LUANIL, In) // notice how this is actually a value rather than nothing

// LUANIL isn't the expected value. It's supposed to be an EasingStyle type

There really isnā€™t a way to fix this issue. Even natively in C++, you canā€™t put a hole when calling a function. Thereā€™s nothing to specify absolutely no value, and globals like NULL or nullptr which are supposed to signify no value donā€™t work when you provide them as arguments:

// trying to skip the second argument
new(2, nullptr, In) // doesn't work
new(2, nullptr) // doesn't work

// solution: provide the default instead
new(2, Linear, In) // works

This is synonymous on how it is done in Luau.

1 Like

I am not sure why the C++ side would be exposed to Lua. In Lua the parameters are passed as a sort of ā€˜tupleā€™ with a certain length, and then presumably calls some C++ function to construct the TweenInfo. Behind the scenes the tuple is somehow unpacked and passed over. But somehow the process invokes a different behavior depending on the length of the tuples rather than the values. My guess is that the C++ side of TweenInfo.new takes the ā€˜tupleā€™ of Lua arguments as a single C++ argument, and acts accordingly, without actually invoking the C++ default argument rules.

I believe the way default parameters work in C++ is by just replacing missing parameters with the default ones at compile time, or by automatically generating an overload that calls the original functions with the default args. But all this happens when the C++ source code is compiled into machine code, but no C++ compilation occurs during the Lua interpretation.

This is intentional behavior. The documentation just doesnā€™t have a way to express the difference between this and legacy functions like Vector3.new.

The fact that old types like Vector3 from early in Roblox history silently handle nils for you is widely considered a terrible mistake because it leads to unintentional behavior in experiences all the time. Long story short if you write Vector3.new(foo, bar, baZ) where baZ is a typo will silently have the wrong value rather than throwing an error.

Unfortunately to maintain backwards compatibility we canā€™t change old types like Vector3, but we can make the new ones work better.

1 Like

Then perhaps Vector3.new() should warn when receiving a nil or a non-number argument that will be treated as 0, without raising an error. So that people can see in the console if they made a potential mistake, but the change would not impact backwards compatibility.

I tried to implement exactly that in the past but it generated way too many warnings, there was no reasonable path to enabling it by default without unnecessarily annoying people.

Recently we were considering adding a default-false Studio setting to enable some of these extended warnings.

3 Likes

In my experience with other languages (Iā€™m looking at you C/C++, PHP, Java/JavaScript), when you use a variable parameter list, you must specify all previous parameters.

The big issue with the compiled languages (C/C++) is all defined parameters must be used because they are pushed onto the stack in reverse order (IIRC, been awhile). When the compiler builds the function, it looks at the parameter list and writes code to push them onto the stack and then writes the CALL instruction into the output stream What many people donā€™t know is that the compiler will also push a dummy value onto the stack first before the parameters. In the function entry code that the compiler inserts, it pushes all the registers that the function uses onto the stack (EXCEPT AX/EAX/RAX) then itā€™s the programmerā€™s code. Before the function returns, the registers are popped off the stack, then the parameters. Remember that dummy value that I mentioned? It holds the return address (the address of the instruction immediately after the CALL instruction). Part of the function preamble code is to move that address from where it was pushed into the location of the dummy value on the stack. The reason why the AX/EAX/RAX register is not saved is because it holds the return value of the function, if any.

TweenInfo.new() is a constructor, so it most definitely is executing code when called even though itā€™s a data type. If something is truly optional, NULL or nil will work to indicate use of a default value, but you have to code for that. Other things that I have found in the documentation with Robloxā€™s LUA variant is that if you specify an optional parameter, you must specify all previous parameters as well., even if they are optional. Hereā€™s an example.. It flat says that if you want to use that last parameter, you have to specify the other one before it.

If you look at the byte code definition for LUA, it looks like a CPU instruction set document for a RISC machine. Roblox is most definitely checking the type of the parameters before it processes them. So in order for the constructor to accept nil as a valid parameter, Roblox has to code that in, which I seriously doubt that they will do.

Basically, I donā€™t really consider this a bug, but is by design with the limitation of how compilers and interpreters work.

nil is evil, and this is very much intentional.

i think a distinction between nil and void needs to be clarified here, even though nil represents nothing in Lua, its still a value, so wrt the C side of things, it treates nil as not nothing.

void meanwhile, is loose in its definition, but lt props up when a function returns nothing instead of nil. You almost never run into it unless youā€™re chaining function calls into a C function, since Lua will turn it into nil any chance it gets otherwise. C meanwhile can very much tell the difference between nil and void.

Now, lets take the same function with two different signatures:

  • TweenInfo.new(1) - This is fine because thereā€™s a defined overload for a single number.
  • TweenInfo.new(1, nil) - This is bad because the second parameter expects EasingStyle, NOT nil.

It gets more interesting if we introduce a function returns void.

local function void() return end

TweenInfo.new(1, void()) -- This statement is ok because Lua trims trailing `void` types from a call, effectively making this `TweenInfo.new(1)`

TweenInfo.new(1, nil) -- Unlike void(), nil is treated as a parameter in C, so it has to handle it as a value.

Hopefully I didnā€™t dwell on the point too much.

1 Like

Here is a thought. For Vector3.new() maybe some people are just using nil instead of 0 for whatever reason, so warning on nil would cause far too many spurious warnings. But any time a value that is not a number, but is also not nil, is passed to Vector3.new, like a Vector3int16, or something like Vector3.new("Part", workspace), that is much more likely to be a mistake than a nil.

Itā€™s much more likely to be a mistakeā€¦ and also a thing people do all the time per the analytics I got at the time :sob:

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.