Table inspection seems to ignore nil entries when debugging. Also missing locals

This is an issue because nil is a very common value for variables to have in Lua.

I know in Lua nil is tricky because it can mean both/any of nil or undefined. But this interface should show all the keys defined on this table, not all the values. So I think it can work better.

Also I noticed that the debugger is not populating all the local variables of my Module Script. For instance, BaseSwordDef is missing.

Not in globals either

10 Likes

But this interface should show all the keys defined on this table(…)

It does. Setting a nil value for a key in a table is not a definition. You could argue that it is a declaration, but in my opinion it’s not even that.
The compiler won’t assign memory for declared but undefined variables (since it doesn’t know how many bytes are needed duh) until the actual definition happens, which is why it doesn’t show up in the debugger.
It is usually considered bad practise to declare variables and not initialising them on the same line.

As for the missing locals the compiler seems to strip debug information from upvalues which it shouldn’t when debugging. Declaring those upvalues as globals will let you watch them though.

It doesn’t but the Locals view only shows Locals, not Upvalues. Perhaps we should combine them.

I come from the world of C, where everything is explicit. The current behavior is very counterintuitive.

If memory in a table is allocated for storing a value, even if it is nil, I want to see it.

Nil != undefined

4 Likes

Adding a watch for something that is in scope feels bad.

What if you had to do that in MSVC?

Maybe add a tab that is for “Everything that is currently in scope”. But make it the default tab, because that’s all anyone is ever going to want to look at.

4 Likes

Setting a value in a table to nil will not allocate any memory to it since it is not a definition but at most a declaration, as I said before. It is not any different in C and C++.

int var1; // declaration, no memory assigned and removed by the compiler if left as is 
int var2 = NULL; // in C: initialises with the value of (int)(void*)0 which is the integer 0. Since lua does not have pointers, nil does not represent the null pointer and therefore cannot cast it to 0
int var3 = 0; // declared, defined and initialised

Lua is tricky. Unlike languages like C# where everything is defined and stays permanent even if a variable is nothing, in Lua, if you set a key in a table to nil, it doesn’t just set the key to nil, it also erases the key itself.

Let’s say I created a table with a single index:

local t = {
    Foo = "Bar"
}

And then I set that key to nil.

t.Foo = nil

It completely erases the field, so the table is now the equivalent of this:

local t = {}

That’s why you don’t see the fields in the debugger.

1 Like

That makes sense but is not satisfying :frowning:

This is a design flaw in Lua that seems to go very deep. For instance,

image

I’m explicitly adding these nil entries to my table “objects” in the hope that Roblox Studio intellisense will pick them up and prevent me from typo-ing them in a future, creating runtime errors.

Look at this crazy stuff

Guess what this outputs:

local count = 0 
local t = { why = nil, whynot = 1, y = {}} 
for _ in pairs(t) do count = count + 1 end 
print(count, #t)

Lua is the only language I know of where you have to think about it. The answer may surprise you.

4 Likes

It happens that Roblox’s intellisense is smart enough to notice these explicit definitions in autocomplete.

image

It just doesn’t extend to the debugger’s variable inspector.

image

Would also be nice if hovering over a nil variable would display nil. Instead it does nothing.

This is a problem with how # is implemented, not how tables actually work. On top of that, I don’t see any scenario where you would need to use # on a mixed table or an array with empty values. For the method the # is implemented there will always be someone out there who won’t like it so it’s also useless to complain about it.

This isn’t a design flaw, you’re not supposed to work around it, you’re not supposed to make that # work, whatever is the number you need, you either need to keep record of it yourself somewhere or calculate it when you need it.

Your data structures’ values should be explicitly defined, you can’t make an arbitrary data structure (like in that example) and expect the # to guess exactly what it is you want. Using it outside of its scoped and intended use and then calling it a design flaw is not right.

It’s not, you have to think about it in C++ datatypes, JavaScript arrays, Python sets and ect…

Nil is not undefined, when anything is set to nil, it means any value held will be deallocated in some time and the key will be destroyed just like ForbiddenJ said, this the expected behavior of any datatype in Lua since there is no actual memory deallocation in Lua so setting it to nil means completely getting rid of it, including the space it held. In Lua, you cannot preallocate data (There are some tricks to confuse gc to make some code more efficient by not letting it deallocate but those are hacks and are outside of the intended use of Lua) so the debugger is right in not seeing them since they don’t actually exist in memory.

1 Like

Disagree. When I use Lua its like I’ve fallen back into the 1980s.

This is how you get a table size in C#:

Hashmap.Keys.Count

Importantly, this performs no computation (O(1) vs O(N) for #) and it is obvious by looking at it how it works. Also the value part of the key/value pairs don’t matter. If they are null they still exist. I can serialize the data structure without writing extra code to trap this case.

I don’t want to manage basic data structures myself. I want to write games. The fact that I need to represent my structs as tables makes this harder.

This short-coming is part of a pattern of general inadequacy for using dynamically typed tables to represent game objects and how it makes things that are easy in other languages difficult/annoying in Lua.

my 2c

5 Likes

Again, there’s no distinction between null and undefined in Lua. This is probably a design flaw, but the decision was made long ago. You could make a feature request for some equivalent to null (or support an existing one if it exists) to fill that gap if you have a compelling enough use case, but as it stands there’s no easy way to distinguish between the two.

That said, you might try using the Typed Lua beta and making feature requests to help shape it to your use. It’s more likely that you’ll get a way to mark a field as uninitialized to the script analyzer than it is that you’ll get a new keyword, so I would recommend this route.

If you have qualms with the behavior of the # operator there’s an existing feature request to make it do what I think you want here:

1 Like

Isn’t the example completely unrelated to game object representation?

In general it feels like you’re comparing apples and oranges when comparing data structures in C# vs Lua. The odd behavior of # comes primarily from the fact that in Lua right now, table length grows automatically when you assign elements to indices (this is common in many other dynamic languages like JavaScript), and shrinks automatically when you remove elements (this doesn’t happen in JavaScript for example).

We might change the semantics of # at some point, there’s a few alternatives here that I’ve been thinking about.

3 Likes

Maybe, but it’s actually a bit complicated. This is a wart, but fixing this wart makes one more appear (the differences between undefined & null aren’t pretty, although in JS it’s more odd because an array cell can be empty, undefined or null!). The balance is tricky. Overall other than # this doesn’t seem actively bad - and I think we can make # nicer in time.

I can relate there. I know another developer who still programs websites in VBScript, which is ancient now.


Lua was designed with flexibility in mind, so it may be possible to write a basic typing library to force lua to behave the way you need, so you could do something like this:

local TL = require(game.ReplicatedStorage.TypeLib)

local MyClass = TL.newClass({
    Ball = {"BasePart"},
    PreferredNumber = {"number", 3.5},
    Bounce = {"string (number, string)", function(self, count, thenSay)
        -- Do whatever here
    end}
    Constructor = {"(BasePart)", function(self, ball)
        self.Ball = ball
    end}
})

local v = MyClass.new(Instance.new("Part"))
v:Bounce(3, "Hello")
v.Ball = "Something Else" -- Error: Attempt to write string to BasePart value Ball.

Not pretty, but it’s the best I could think of off the top of my head.

I’m inclined to agree – I’ve only rarely come across times where the distinction would be helpful. It might be worth doing for Luau though just because there is a distinction worth making for analysis purposes. In this case, for example, it would make life easier if there was a way to set something’s value to exists but is empty. Not sure if that’s something the functional part of the language should support though; might be better to make it part of the type system.

This is a bit off topic, since we’re start talking about functional changes, it made me think of it. Luau is becoming less and less like stock Lua. Between things like type annotations and language features like continue, at what point is Roblox Lua a ‘subset’ language that should have its own file extension and whatnot?

1 Like

I don’t think we want Lua to become more like JavaScript. Lua’s simplicity is one of its few advantages compared to other languages, and we’ll begin to lose this advantage if Lua is pushed to conform to another language’s standards (like by adding undefined, or a “right” way to do OO.)

Perhaps table prototyping could be done by the VM, and fields that are defined explicitly (e.g. {Foo = nil}) could be allocated such that they use 16 bytes for each field instead of 40, and so nil values are visible when debugging? This would be a pretty cool optimization that also fixes the debug problem, and I think something like it was mentioned during RDC, but for a quick fix one could use false instead of nil.

2 Likes

A solution would be to add an “empty” type and global, which would be logically falsey, but only set explicitly, so no change to the behavior of nil

The biggest issue is that the “==” operator is useless. Otherwise I don’t think this would be a big problem…

In JavaScript yes, in Lua no.

In Lua nil absolutely does mean undefined, the fact that these mean the same thing is a core part of the language’s semantics, which is definitely not feasible to change. This goes as far as the way to shrink a table being setting the values in the table to nil.

1 Like

The common thread is I’m trying to write OOP-ish code and everything is terrible.

Here’s another one - Table:copy and other standard operations

We have a five minute tutorial for all of our devs to waste time reading instead of providing basic functionality out of the box.

2 Likes