JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Help with Mistakes

Jul 24, 2025 • [luametatablestesting]


When we program, many if not most of our mistakes fall into a few categories. I set out to get help with one of mine by improving my tools.

One of my most common mistakes is to mistype a class member name. When it’s the name of a method, I quickly get an error about trying to call a nil. When it’s a fetch, it fetches a nil and I quickly get an error trying to add nil to a number or something. But when it’s an assignment, Luau just goes ahead and does it, and then later on, the value of the thing I was trying to store into isn’t updated. And yet, I can see the updating code Right There. It often takes me a long time to recognize the typo.

One of the great things about Lua and Luau is that the language’s metatables often allow us to change how things work to our benefit. We have already created a class() function that helps us create object-oriented code, and the little tests and expectations frameworks that facilitate writing tests that exercise our code and help us ensure that it does as we intend.

Among the metamethods in Luau, there is __newindex. This is a function that will be called when our code tries to create a new keyed entry in a table, and there is not already an entry with that key there. Normally, __newindex just goes ahead and adds the key. But it could do anything … including throw an error.

Now there are really two ways of programming a class in Lua, in this context, and I don’t want to force people into a particular way. The two I have in mind are:

I find that my code is easier to understand when I use the first method. Naturally, I am evolving the code, so I go back many times and add a new member or remove an old one from my init, but I like having everything that the class is going to reference being declared right there. It looks, for example, like this:

Bezier = class()
function Bezier:init(p0, p1, p2, p3)
    self.p0 = p0
    self.p1 = p1
    self.p2 = p2
    self.p3 = p3
end

To the extent that I follow my rule above, I can read the class definition and init and be confident that the only data members in a Bezier are the four points p0-p3.

Now it happens that in programming the Bezier, I occasionally found myself thinking about points p1-p4, probably because Lua indices start with one, not zero. I’d like to ensure that I’d get a run-time error if some code down below were to say

self.p4 = something

A more strict language would give a compile-time error, but that’s not Lua. I, for one, prefer it that way, but others prefer a more strict language. But I do want to be able to catch that error.

My plan was to add a method to every class, so that we can say this:

Bezier = class()
function Bezier:init(p0, p1, p2, p3)
    self.p0 = p0
    self.p1 = p1
    self.p2 = p2
    self.p3 = p3
    self:final()
end

Saying self:final() should change the object we’re working on so that attempts to define new variables, like p4, will be disallowed.

With this code:

Bezier = class()
function Bezier:init(p0, p1, p2, p3)
    self.p0 = p0
    self.p1 = p1
    self.p2 = p2
    self.p3 = p3
    self:final()
end

function Bezier:at(t)
   -- de casteljau algorithm
    self.p4 = 0 -- intentional error to demonstrate final
   local r1, r0 = self:tangent_at(t)
   return r1*t + r0*(1-t)
end

My tests issue this error:

lua:634: cannot create field p4=0. [310] 
Test: 'test_pathfinder_polyline_path'

And that is just what I wanted. Here’s how the class function was modified to do the job. Scan down to where I’ve put all the dashes:

function class.class(base)
   local c = {}    -- a new class instance
   if type(base) == 'table' then
       -- our new class is a shallow copy of the base class!
       for i,v in pairs(base) do
           c[i] = v
       end
       c._base = base
   end

   -- the class will be the metatable for all its objects,
   -- and they will look up their methods in it.
   c.__index = c

   -- expose a constructor which can be called by <classname>( <args> )
   local mt = {}
   mt.__call = function(class_tbl, ...)
       local obj = {}
       setmetatable(obj,c)
       if class_tbl.init then
           class_tbl.init(obj,...)
       else
           -- make sure that any stuff from the base class is initialized!
           if base and base.init then
               base.init(obj, ...)
           end
       end

       return obj
   end
-- -----------------------------------
   c.final = function(self)
        local parent_mt = getmetatable(self)
        local child_mt = {
            __index = c, 
            __newindex = function(t,k,v) 
                error("cannot create field "..tostring(k)..'='..tostring(v), 2) 
            end
        }
        setmetatable(self, child_mt)
    end
-- ----------------------------------
   c.is_a = function(self, klass)
       local m = getmetatable(self)
       while m do
           if m == klass then return true end
           m = m._base
       end
       return false
   end

   setmetatable(c, mt)
   return c
end

Let’s look at that bit in detail:

   c.final = function(self)
        local parent_mt = getmetatable(self)
        local child_mt = {
            __index = parent_mt, 
            __newindex = function(t,k,v) 
                error("cannot create field "..tostring(k)..'='..tostring(v), 2) 
            end
        }
        setmetatable(self, child_mt)
    end

Defining a method final on c adds that method to the metaclass of our object, which means that all instances will understand final. What does the method do?

  1. It gets the metatable of the object in question. (This is probably c but let’s not assume that.)

  2. It creates a new metatable, whose __index is the parent metatable. This ensures that our new metatable will understand all the methods of the class.

  3. The new metatable also includes a function __newindex which simply raises an error, printing some of the information that is sent to __newindex by Lua. (One looks that up in the documentation.)

  4. We set the object’s metatable to be our new one, which gives it the new method __nexindex plus everything in the parent.

  5. Henceforth this object will give that error message if we try to store into a member that doesn’t already exist.

In use, we can close out our object’s initialization with a call to final and it is protected against accidentally storing into non-existent members.

Caveats
There are two caveats: In Lua, assigning a nil to a table entry removes the entry, it doesn’t leave an entry whose value happens to be nil. Therefore, when we use this feature we have two constraints:

It will not suffice to declare a member like self.member=nil. We must provide a non-nil value if we want to use final.

We cannot set a member to nil as a signal or for any other reason, if we intend to set it non-nil later.

For now, I’ll live with this feature and see if I like it. If not, we can change it or remove it. We’re here to make our job easier, not to follow some arbitrary rules.

Safe paths!