C Stack overflow when trying to index table

I’m using a metatable and have a custom __index metamethod defined here:

local option = {}

function option.__index(self, key: any)
	if key == "is_some" then
		return self.__value ~= math.huge + 1
	elseif key == "is_none" then
		return self.__value == math.huge + 1
	end

	return self[key]
end

Whenever I try calling a method on the option object it just gives me a stack overflow error.
How do I stop this? Using rawget() just removes the metatable part.

3 Likes

__index is being cyclically invoked when you try to use self.__value. Try rawget within the __index metamethod.

1 Like

Same error:

function option.__index<T>(self, key: any)
	if key == "is_some" then
		return rawget(self, "__value") ~= math.huge + 1
	elseif key == "is_none" then
		return rawget(self, "__value") == math.huge + 1
	end

	return self[key]
end
1 Like

My bad, I think I had it backwards and it’s due to the final return statement.

local option = {}

function option.__index<T>(self, key: any)
	if key == "is_some" then
		return self.__value ~= math.huge + 1
	elseif key == "is_none" then
		return self.__value == math.huge + 1
	end

	return rawget(self, key)
end

-- Testing
local obj = setmetatable({}, option)

print(obj.is_some)
print(obj.is_none)
print(obj.None)
2 Likes

My workaround was to put all the custom methods into the new() function.
Before it was something around the lines of:

local option = {}
-- index stuff

function option.Unwrap(self)
    -- Do something
end

Which meant that rawget(self, key) would ignore the option metatable.
I still don’t know a fix to this. :pensive:

1 Like

@Operatik was right in 2 different posts…

you need rawget for all indexes of self, in the if-elseif statement and in the return statement because they all cyclically invoke the __index metamethod. Here’s a fixed version:

--not sure what dynamic type <T> was for so removed it
function option:__index(key: any)
    if (key == "is_some") then
        return rawget(self, "__value") ~= math.huge + 1
    elseif (key == "is_none") then
        return rawget(self, "__value") == math.huge + 1
    else
        return rawget(self, key)
    end
end

For the first two statements, they should be generally fine since the key cannot be two at the same time. The final case is the fix.

Yes, but even though it can’t be both at the same time, both branches index self.__value, where self points to obj, so __index is re-invoked no matter the branch.

1 Like

Code_NR7T2hgDWl

Here’s the module:

local option = {}

function option.__index<T>(self, key: any)
	if key == "is_some" then
		return self.__value ~= math.huge + 1
	elseif key == "is_none" then
		return self.__value == math.huge + 1
	end

	return rawget(self, key)
end

function option.new(value)
	return setmetatable({
		__value = value
	}, option)
end

function option.unwrap(self)
	return self.__value
end

return option

It’s able to return __value when I get it directly.
But when I try running the option function it doesn’t work.

I found another work around (potential solution).

local CustomMethods = {}

function CustomMethods.unwrap(self)
	return self.__value
end

function option.__index<T>(self, key)
	if key == "is_some" then
		return self.__value ~= math.huge + 1
	elseif key == "is_none" then
		return self.__value == math.huge + 1
	end

	return setmetatable(table.clone(self), {
		__call = CustomMethods[key]
	}) or rawget(self, key)
end

I keep all the methods in a CustomMethods function. When I call a custom method then it just returns a new metatable using the original self.

My question is why not:

  • Just have actual methods in your tables
  • Just have actual methods in your __index in table shorthand

Your setup is somewhat convoluted and I just have to ask, to what end? I hope you consider if you really need to implement it in this way, as it definitely isn’t this complicated to just get a monad for nil.

The reason it says ‘Attempt to call a nil value’ is because when __index is run, it never returns the function from the metatable. It just returns the key of option from the table itself, which is nil. Remember, the method is in the metatable, not the table.

1 Like

Exactly, that’s why the error occurs. __index either causes a stack overflow, or rawget() voids the metatable part entirely.

I just put the methods in the new function now so it doesn’t do any of that weird stuff.
I don’t think there’s a fix.

Can’t you just make it search the metatable for the method if it’s not one of your specific keys?

Yeah but that’s just the metatable, not the self table. And I can’t run the function since __index doesn’t work with functions.

That’s not true. __index does work with functions. If it didn’t, it’d break a lot of Lua OOP.

I meant

function tab.__index(self, key, ARGS)
    -- ARGS arent a thing but `__index` does get called when trying to call the function
end

Yes, but __index returns the function and then the args are passed to that. The function being returned and it being called happen separately.

1 Like

tab.__index = {} is a special cased shorthand, in any other case you’d need to use setmetatable or getmetatable.