Syntactical Differences in Function Definition in LuaU versus Lua

Not necessarily for help, but I’m just curious if anyone knows if this is intended or not:

When defining/declaring a function in regular Lua, the declaration type (see below) is syntactic sugar - they all act the same and will do the same thing when getting debugging info about them. However, in LuaU, i’ve noticed an oddity that I’m not sure is intentional and want to see if anyone has any insight on, and if I should attempt to make a bug report on it.

local function f() 
	print(debug.info(1, "n")) -- prints 'f' as expected
end

function g()
	print(debug.info(1, "n")) -- prints 'g', as expected
end

local lf = function()
	print(debug.info(1, "n")) 
    -- [[Here's where things get.. odd...
    -- for some reason, this prints "nil" when doing tostring, or
    -- "" when regularly printing.]]--
end

lg = function()
	print(debug.info(1, "n"))
    -- Oddly enough, same thing here! What gives?
end

f() -- 'f'
g() -- 'g'
lf() -- ""
lg() -- ""

And if anyone’s a bit skeptical:

  00:29:04.952  f  -  Server - Script:2
  00:29:04.952  g  -  Server - Script:6
  00:29:04.952    -  Server - Script:10
  00:29:04.952    -  Server - Script:14

Does anyone have any insight as to whether this is intentional or not? Should I make a bug report? Has there been a report about this in the past?

1 Like

I didn’t see any issues about this on the GitHub, nor does this behavior seem intended, so you should be good to open an issue there.

P.S. It’s always “Luau”, never “LuaU”. :slight_smile:

3 Likes

i just say LuaU out of habit, though I also say Luau dw lol (it’s just a word in and of itself so I avoid confusion)

anyways thanks! i’ll open one and see how it goes, will update

Those are anonymous functions. Same thing as

Signal:Connect(function()
    -- this is an anonymous function
end)

Reason being local x = function() assigns a nameless function to a local variable
local function x() actually gives the function a name however the function only exists in the local scope
function x() same thing but now it’s in the global scope

think of it as

local function x()
end

local y = x

you’re not renaming the function to y because you assign it to the y variable, you just store the already named function in a differently named vatiable

1 Like

https://www.lua.org/pil/6.html#:~:text=If%20functions%20are%20values%2C%20are%20there,function%20(x)%20return%202*x%20end

That is Lua, not Luau. They compile slightly differently, one of the differences being that variable names don’t get compiled (but function names do). lf = function defines the VARIABLE lf as a function, so the name doesn’t get compiled, and returns nil when used with debug.info

1 Like

Which should change nothing

Luau derived from Lua 5.1, which had the same exact “syntactic sugar” I’m talking about

Which is why this is probably intentional. Since it’s derivative not an exact clone and made to work well with robloxs engine in terms of memory and other features they’ve implemented.

1 Like

Which… still changes nothing. The way a function is declared really shouldn’t be changed like this - even if it’s a niche change.
Functionality should still be preserved if at all possible

debug.info(level, “n”) retrieves the function name, if known. But the key is: the name must be statically detectable by the VM at the time the function is created not dynamically guessed.

local function f() ... end
function g() ... end

These are both declarations with names, so Luau internally stores the name “f” and “g” along with the function object.

local lf = function() ... end
lg = function() ... end

Here you’re assigning anonymous functions to variables.

Even though you’re assigning them to lf and lg, the function object itself has no name internally you’re just binding an anonymous function to a variable after it’s already been created.

2 Likes

Yep, that’s what I figured. Don’t know why it behaves that way, because it should not

The function methodName() syntax is actually intended to be syntactic sugar. Ideally, Luau should have the same behavior

which clearly it couldn’t be preserved. It could be for many reason that we’ll probably never know why. But I’d guess it has to do with anonymous function within connections, compile time or something completely different. Nonetheless clearly they didn’t keep it and it has it’s reasons.

It’s also such a small and useless change that even if you were to mark it as a bug or a feature request I doubt roblox would go out of their way to change it. However if you feel you need this to be a feature go ahead.

2 Likes

The likely reason is :Connect(function() end), but even so, it should be pretty straightforward to maintain the syntactic sugar standard Lua has for declaring functions.

It should be very easy to preserve this feature

Luau (and Lua in general) treats this:

local foo = function() end

as:

“Create an anonymous function object, then assign it to the local variable foo“
Whereas:

local function foo() end

Is more like:

“Define a named function foo, store it as a local variable.”

Also sorry if i take a while to reply, I’m on mobile and i didn’t sleep at all today.

Oops, didn’t mean to assign this as solution. It’s 2:12 here, so… yeah

In Lua, though, with the first line you showed, it’s just the “less nice” way of doing “function foo()” or “local function foo()”

The non syntactic-sugar way is actually very convenient at times, particularly for tables, because syntactically, it’s kind of counterintuitive to go:

function tableName:methodName()

as compared to the regular indexing method

tableName.MethodName = function(self: typeof(tableName))

However, because debug.info() does not treat these the same, in certain cases, I have to go with the “syntactic sugar” method.

It’s not particularly significant, but it appears to be unintended behavior

1 Like

Mostly everything in luau is made with luau, including things like connections. They probably didn’t want to accidentally rename a function when they handle connections or just couldn’t implement it at the time without renaming them. Should be easy yes, but they weren’t able to and now it’s been such a long time that the code probably relies on it in multiple places. Again, I doubt roblox would do anything about this for their own sake and at this point it is an intentional change that comes with luau.
If you really need to use debug info “n” then just declare the function like intended. I don’t see why this is a necessary change and why roblox should put time and money into changing this when they have way bigger problems.

2 Likes

You’re right, this is unintended behaviour but then again you could do some work around to manually attach names to anonymous functions like some “wrapper” module i thought off the top of my head

You can simulate this by wrapping functions and attaching a fake “name” that your debug system reads instead of relying on debug.info.

Whenever i wake up I’ll check out this further

1 Like

This is actually caused by the heart of how Luau treats function objects.

On the C side, Luau has this internal structure called a Proto. This contains the entire blueprint for the function - upvalues, bytecode, metadata, constants, et cetera. But the main thing is this field:

TString* debugname;

this is the field containing the string that’s returned by debug.info(1, "n").

This won’t get populated from assigning an anonymous closure to a variable. But why?


Well, take the second syntax example:

local foo = function() end

This tells the compiler:

  1. Build an anonymous Proto
  2. Emit a CLOSURE instruction with no name attached
  3. Perform a separate SETLOCAL assignment to assign it to the variable

Here, the closure and the assignment are separate instructions, and Luau follows a register-based VM system - the value must be in a register before it can be assigned to a variable. So, Luau creates the Proto, puts it into a register, and then assigns it to the local. This Proto is already created, and since Luau is optimised for performance, it does not go back to modify the Proto - so no debug name is assigned, because there was none present at the time of creation. It’s important to note debugname isn’t required for execution, giving Luau even more reason to just omit it entirely.

But, with the named example:

local function foo()
end

the compiler sees the full declaration as one. It will:

  1. Create a Proto tagged with the debugname
  2. Emit a FUNC instruction with that association

so you basically end up with:

  • Anonymous function assignment - Proto has no debugname
  • Named function - Proto has an associated debugname

Full closure struct
typedef struct Proto
{
    CommonHeader;


    uint8_t nups; // number of upvalues
    uint8_t numparams;
    uint8_t is_vararg;
    uint8_t maxstacksize;
    uint8_t flags;


    TValue* k;              // constants used by the function
    Instruction* code;      // function bytecode
    struct Proto** p;       // functions defined inside the function
    const Instruction* codeentry;

    void* execdata;
    uintptr_t exectarget;


    uint8_t* lineinfo;      // for each instruction, line number as a delta from baseline
    int* abslineinfo;       // baseline line info, one entry for each 1<<linegaplog2 instructions; allocated after lineinfo
    struct LocVar* locvars; // information about local variables
    TString** upvalues;     // upvalue names
    TString* source;

    TString* debugname;
    uint8_t* debuginsn; // a copy of code[] array with just opcodes

    uint8_t* typeinfo;

    void* userdata;

    GCObject* gclist;


    int sizecode;
    int sizep;
    int sizelocvars;
    int sizeupvalues;
    int sizek;
    int sizelineinfo;
    int linegaplog2;
    int linedefined;
    int bytecodeid;
    int sizetypeinfo;
} Proto;
1 Like

Quite insightful, thank you! I’ve learned a bit of C, so luckily I’m able to understand at least sections of that. Quite unfortunate that it doesn’t go back to set debugname, because in certain cases, it’s more syntactically convenient to define tableName.MethodName as compared to function tableName[: ? .]methodName()

I wonder if it would really add any compilation time for luau to define debugname?

There’s not many specific cases, but as said, it’s more syntactically convenient.

Plus, it would follow Lua’s (which is what Luau is derived from) standards and functionalities better. Unforunate, though.

1 Like

it’d actually be a real pain to assign a debug name to it because of how Luau works.
By the time the assignment to the local variable is run, the function is gone from the registers. This means that Luau would then need to grab it again, put it in a register, put a string in another register, then perform another instruction to assign it. Not to mention, you’re opening back up an already closed Proto object. So you end up with about 7 more instructions, some of them bulkier than others.

All of this put together would cause considerable performance issues, so it doesn’t really seem worth it, as most use cases for this is just debugging. There’s a couple of use cases which actually exploit this behaviour - if you want to make it harder for an exploiter to reverse a system of yours, you can use anonymous assignment to basically just hide the name.


If there’s any bits of my explanation you don’t understand, I’m happy to explain it further.

2 Likes