I made a code snippet that makes all global variables immutable, with an optional tag-like thing in the variable name (“m_”) which functions similarly to Rust’s mut keyword, or the opposite of const (where things are const by default).
The goal was to make it so that all variables that are redefining create an error saying that the variable cannot be changed. However, the method I used (seeing if they’re added to the global table) doesn’t work in Roblox because Roblox’s _G behaves differently.
Here’s the code:
local function immut(item)
return setmetatable({}, {
__index = function(_, key)
return item[key]
end,
__newindex = function()
error("Attempt to redefine immutable object " .. tostring(item))
end,
})
end
setmetatable(_G, {
__index = function(_, key)
return rawget(_G, key)
end,
__newindex = function(_, key, value)
if tostring(key):sub(1, 2) == "m_" then
rawset(_G, key, value)
else
rawset(_G, key, immut(value))
end
end,
})
thing = {ok = "test1"}
print(thing.ok) --> test1
pcall(function()
thing.ok = "hm" -- will error because it's being redefined
end)
m_thing = {ok = "yes"} -- m_ is for mutable
print(m_thing.ok) --> yes
m_thing.ok = "no" -- won't error, because it has "m_"
local thing2 = {yes = "ok"}
print(thing2.yes) --> ok
thing2.yes = "no" -- won't error, it's a local variable so it won't get added to _G.
Basically, in Roblox, when you define a variable both with and without local it doesn’t get added to the global table (actually with the new script analysis I’m pretty sure it warns you when you define a variable without local).
But in the code, I rely on that functionality to make every variable automatically immutable, so I want to know how I refactor my code so it doesn’t rely on that.
I don’t believe you can do this in Roblox. My first idea was to use getfenv to get the calling environment of the script. I came up with this implementation:
local DefinedMutability = {};
DefinedMutability.mutablePrefix = "m_"
local function __index(callingEnv, key)
return rawget(callingEnv, key);
end
local function __newindex(callingEnv, key, value)
local startIndex, endIndex = key:find(DefinedMutability.mutablePrefix);
if startIndex == 1 then
if key:len() ~= endIndex then
rawset(callingEnv, key, value);
else
local message = string.format(
"Cannot declare a variable by the name of %q",
key
);
error(message, 2);
end
end
local message = string.format(
"Cannot assign to %q because it is immutable."
.. "To make a global mutable, prefix it with %q",
key,
DefinedMutability.mutablePrefix
);
error(message, 2);
end
function DefinedMutability.apply()
local callingEnv = getfenv(1);
setmetatable(callingEnv, {
__index = __index,
__newindex = __newindex,
});
end
return DefinedMutability;
This didn’t work, as I was reminded of the __metatable field. Although the main issue is that the metatables for those environments are locked, therefore you can’t modify the metatable through “setmetatable”. I believe your best bet to “implement” this would not be at runtime, but through some static checker.
I’d also suggest some static checker because magic code like this can become very troublesome.
Calling getfenv or setfenv in your scripts disables a handful of good Luau optimizations. This should also be taken into account before going on to mess with the Lua environment.
I’ve recategorised this thread over to Scripting Support. Please remember that Code Review is for improvements of already working code, while Scripting Support is for resolving programming issues. More information is available in our category guidelines.