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:
- Build an anonymous
Proto
- Emit a
CLOSURE instruction with no name attached
- 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:
- Create a
Proto tagged with the debugname
- 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;