Have you ever considered whether functions in Lua are mutable or not. In Lua,objects are mutable because properties and metatables can be changed. Stringsand numbers are examples of types that aren’t mutable:string libraryfunctions return new strings, numeric operators return a new numbers.
For something to be mutable it must have state that can be changed. Dependingon the version of Lua, there are either two possibilities:
Lua 5.2 replaced the function environment with a specially named upvaluecalled
_ENV. You can read more about this in my companion guide:Implementing setfenv in Lua.
When assigning a function to a new variable it is not copied. Just liketables, function values are actually pointers to a function.
locala=function()endlocalb=a-- these point to the same functionassert(a==b)This is commonly confused withpass by reference. Pass by reference isslightly different. Lua uses pass by value, it’s just that some values arepointers to the same object.
A cloned function will allow you to change state without affecting other codethat is holding references to the original function.
You might think that because Lua is a single threaded language you can modifythe state of the function while it executes, then put it back. This would be trueif Lua didn’t have coroutines.
A running function might yield at any point in execution, and in that time thefunction could have its state changed before the coroutine resumes.
string.dump andloadstringThestring.dump function returns a binary representation of a function as astring. By dumping a function to a string and then reloading it you've createda clone of the function:
localfunctionsay_hi()print("Hi!")endlocalsay_hi_clone=loadstring(string.dump(say_hi))say_hi_clone()--> Hi!assert(say_hi~=say_hi_clone)This works in the previous example but it’s not entirely correct. What aboutupvalues? An upvalue’s reference can not be encoded into the string dump andpreserved when it’s loaded again.
localmessage="Hello"localfunctionsay_message()print("message: "..tostring(message))endlocalsay_message_clone=loadstring(string.dump(say_message))say_message_clone()-- message: nilA new set of upvalues is created for the loaded function, and they all point tonil.
Lua 5.2 and above give two ways to set upvalues on a function:debug.setupvalue anddebug.upvaluejoin. As we discovered inthesetfenv implementation guide, upvalues are shared among multiplefunctions. Changes to the values pointed to by an upvalue should reflect in allthe functions that have access. For that reasondebug.upvaluejoin will beused to connect the original function’s upvalues to the new function.
debug.upvaluejoin takes two pairs of function and upvalue index. Since onefunction is a clone of the other, the upvalue positions will be the same. It’sjust a matter of iterating through all the valid upvalue indexes and joiningthem.
localmessage="Hello"localfunctionsay_message()print("message: "..tostring(message))endlocalsay_message_clone=loadstring(string.dump(say_message))locali=1whiletruedo-- see if i is a valid upvalue indexlocalname=debug.getupvalue(say_message,i)ifnotnamethenbreakend-- join the clone and the originaldebug.upvaluejoin(say_message_clone,i,say_message,i)i=i+1end-- the clone now has a functional upvaluesay_message_clone()-- message: Hellomessage="MoonScript"say_message_clone()-- message: MoonScriptclone_function implementationNow all that’s left is to write a generic function for cloning any function:
localfunctionclone_function(fn)localdumped=string.dump(fn)localcloned=loadstring(dumped)locali=1whiletruedolocalname=debug.getupvalue(fn,i)ifnotnamethenbreakenddebug.upvaluejoin(cloned,i,fn,i)i=i+1endreturnclonedendAs far as I know Lua 5.1 does not provide a way tojoin upvalues. LuaJIT doesprovite an implementation ofdebug.upvaluejoin though, so that may handle anyLua runtimes you run code in.
The best alternative for Lua 5.1 is to usedebug.setupvalue. The result willbe a function that works, but because the upvalues aren’t connected some hardto debug issues may result.
leafo.net · Generated Sun Oct 8 13:02:35 2023 bySitegenmastodon.social/@leafo