LuaVela Immutable Objects¶
Introduction¶
This article describe support for immutable objects in LuaVela. The feature was released as a part of LuaVela 0.14.
Motivation¶
Consider the following cases:
- Implementing read-only objects (first of all tables).
- Protecting the environment making it read-only (e.g. to ensure security when running some sandboxed code).
Lua provides no language-level facilities to create immutable tables, but both problems can be addressed using metatables. However, LuaVela already has a mechanism of sealing that combines two features: Sealed objects (a) are not garbage collected and (b) are immutable.
It means that all Lua code executed by LuaVela already pays the price of run-time immutability checks, but it also has to pay an extra price in case of using metatables for solving problems like the ones mentioned above. This leads to the idea of decomposing sealing into two independent features and exposing interfaces for creating immutable objects to the client code. Of course, mechanism of metatables must be left intact to preserve compatibility with the language.
For a broader discussion of immutability in general, see here.
For a broader discussion of immutability in Lua, see here.
Terms and Definitions¶
- Immutable object
- An object is immutable if it is impossible to mutate its state through any of the platform’s interfaces. An object is called mutable otherwise. Note that the concept of “object state” in this definition does not include a “private” part of the object that is accessible only within the platform. Please make no assumptions about immutability of this private part.
Interfaces¶
Lua-level: ujit.immutable
¶
v = ujit.immutable(v)
Makes value stored in v
immutable in-place. Returns v
for convenience. The table below outlines effects of applying this interfaces to values of all basic Lua types (listed according to the Reference Manual):
Type Description nil
No effect: All values of this type are always immutable. boolean
No effect: All values of this type are always immutable. number
No effect: All values of this type are always immutable. string
No effect: All values of this type are always immutable. function
No effect: All values of this type are always immutable. userdata
No effect: All values of this type are considered immutable. thread
Not supported: cannot be made immutable, a run-time error is thrown. table
Tables are made recursively immutable unless no key or value is of the type that cannot be made immutable (see above). If there is a key or value of the type that cannot be made immutable, a run-time error is thrown. If a table has a metatable, this metatable is made recursively immutable, too.
Notes on Immutable Tables¶
Since immutability is applied recursively, a table nested into some immutable table is also immutable:
local tbl = ujit.immutable{ foo = { bar = "baz" } }
local foo = tbl.foo
tbl.x = "y" -- ERROR!
foo.y = "x" -- ERROR!
However, all tables that are created anew are mutable by default, so creating a deep copy of an immutable table will not preserve immutability.
Besides, following is true for immutable tables:
- Immutable tables can have metatables. However,
__newindex
metamethod will not be reachable for immutable tables.- After a table is made immutable, new metatables cannot be set for the table. In particular, a metatable cannot be unset for the table that had that metatable at the time of being made immutable.
- After a table is made immutable, the contents of its metatable is also immutable.
Rationale: A table without a metatable represent some piece of data. A table with a metatable represents some piece of data and some additional semantics on this data. For the sake of consistency, we should force immutability on both data and any additional semantics if it is defined.
Legacy Notes on Functions and userdata
¶
For functions, following was true prior to LuaVela 0.16: If a function has no upvalues, applying this interface has no effect. Otherwise applying this interface is not supported, and a run-time error is thrown.
For userdata
, following was true prior to LuaVela 0.16: userdata
could not be made immutable, a run-time error was thrown.
Preserving Consistent Object State¶
If an attempt to make an object immutable fails, it is guaranteed that its state will be left intact:
local co = coroutine.create(function () end)
local t = {
nested = {
nested = {
oops = co, -- expected to fail
},
},
}
local status, err_msg = pcall(ujit.immutable, t)
assert(status == false)
t.foo = "bar" -- OK
t.nested.foo = "bar" -- OK
t.nested.nested.foo = "bar" -- OK
In particular, if a mutable object holds a reference to an immutable object and fails to become immutable, state of both objects is preserved:
local co = coroutine.create(function () end)
local t = {
nested = {
nested = ujit.ummutable{},
oops = co, -- expected to fail
},
}
local status, err_msg = pcall(ujit.immutable, t)
assert(status == false)
t.foo = "bar" -- OK
t.nested.foo = "bar" -- OK
t.nested.nested.foo = "bar" -- ERROR!
Immutability and Sealing¶
Sealing implies immutability: If an object is made immutable and if sealing can be applied to that object, this object will be successfully sealed. If an object is successfully sealed, an attempt to make it immutable is a no-op.
Immutability and Garbage Collector¶
Immutable and mutable objects are treated equally by garbage collector.
Making Immutable Objects Immutable Again¶
Applying this interface to the already immutable object has no effect.
C API: luaE_immutable
¶
void luaE_immutable(lua_State *L, int idx);
Makes the value stored at index idx
immutable in-place. It has exactly the same semantics as the Lua-level interface.
Turning Immutability Off¶
There are deliberately no interfaces for making immutable objects mutable again. Here is an example why introducing such interface is potentially dangerous:
local t = ujit.immutable{ a = { b = "c" }, foo = "bar" }
local x = ujit.mutable(t.a) -- contents of t.a is now mutable, but t is in inconsistent state
Support by JIT Compiler¶
Prior to LuaVela 0.18, a call to ujit.immutable
was not JIT-compiled. As of LuaVela 0.18, a call to ujit.immutable
is JIT-compiled.
Implementation Notes¶
No extra notes currently.
Performance Considerations¶
Please remember that marking an object immutable requires a full traversal in case of tables. It means that using the features for large tables or tables with deep nesting should be done carefully, and performance of according code must be monitored closely. However, please note that accessing immutable objects will not bring any overhead since appropriate checks are already in place to support sealing (see above).
Discussed and Abandoned Ideas¶
Semi-read-only Tables¶
There was an idea to implement “semi-read-only tables” that would allow insertion and prohibit modification of existing values:
local t = semireadonly{a = "b", c = "d"}
t.e = "f" -- OK
t.e = "F" -- ERROR!
t.a = "B" -- ERROR!
However, the idea was abandoned because of following reasons:
- This approach erodes the idea of immutability in principle.
- A need to implement such interface indicates that the bad code design.