__update metamethod — intercept writes to existing keys (patch for 5.5.0)

182 views
Skip to first unread message

chen chen

unread,
Apr 8, 2026, 6:23:46 AM (5 days ago) Apr 8
to lua-l
Hello,

I've been thinking about the gap between `__newindex` (new keys only) and the need to intercept updates to existing keys. The proxy-table workaround works but has notable overhead. I put together a small patch (~95 lines) for Lua 5.5.0 to explore what a native solution might look like — I'd love to hear if this direction has been considered or if there are concerns I'm missing.

The patch adds an `__update` metamethod that fires when assigning to an existing key (including deletion via nil), complementing `__newindex`. Same-value writes are skipped as a fast-path optimization.

```lua
local t = setmetatable({}, {
    __update = function(t, k, v)
        rawset(t, k, v)  -- or do validation, logging, etc.
    end
})
t.x = 1   -- fires (new key, no __newindex)
t.x = 2   -- fires (existing key, different value)
t.x = 2   -- no fire (same value, fast-path skip)
t.x = nil -- fires (existing key, deletion)
```

This eliminates the proxy-table pattern (`__newindex` + `__index` + `__pairs` on an empty table). Tables without `__update` have zero overhead — a single bit flag gates the fast path.

Full proposal, implementation details, and benchmarks:
https://github.com/Ne9roni/lua-update-metamethod

Feedback welcome!

Martin Eden

unread,
Apr 8, 2026, 10:02:28 AM (5 days ago) Apr 8
to lu...@googlegroups.com
Hello Chen,

I like concise text of your proposal.

Conceptually __update encompasses __newindex. In other words
__newindex is custom case of __update.

I understand that in add-on proposal you can't redesign things.
But still want to highlight discrepancy between conception and
implementation. In implementation __newindex and __update are
separate.

So if for some imaginary table we have all-forbidding __update and
all-allowing __newindex we can always add new items but not change
them.


And yeah, I'm not happy with Lua table metamethods. (Still love you Lua!)

__index and ___newindex are called only for not-found key.
Why this non-existence criteria? Why not throw them away in 2026 and
add read and write wrappers for table slots? Like OnRead(), OnWrite()
(or __read, __write if you like underscores)?

Thirty years ago associative array with exception callbacks was novel
and practical. Is it now? Can some essential changes ever happen?

-- Martin

bil til

unread,
Apr 9, 2026, 4:24:52 AM (4 days ago) Apr 9
to lu...@googlegroups.com
I also like this idea, if I am allowed to comment :).

(I am not sure about the solution presented... of course it should be
added only if it is not adding too much overhead).

(I also recently considered to add a bit handling metaclass in form of
"array style" - but finally I decided against array style, as in this
case I preferred to create new bit fields with a special "init / open
access function", and not allow creating them just by definition of a
new array element (in case of such new meta functions - I want to
avoid the possibility that a new class is defined by accidential name
typing error)).

(so if there would be the possibility to use a function like
"__update", but NOT support "__newindex", this would solve my
objection here).

Am Mi., 8. Apr. 2026 um 12:23 Uhr schrieb chen chen
<georgech...@gmail.com>:
> --
> You received this message because you are subscribed to the Google Groups "lua-l" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/lua-l/e5ca8f49-c6d6-42c7-8524-11308ef59914n%40googlegroups.com.

Yan

unread,
Apr 9, 2026, 6:52:49 AM (4 days ago) Apr 9
to lua-l
It seems that in Lua 5.5, the `isrealasize` is no longer in use, and the corresponding comments should be able to be removed.

```
src/ltable.c:** dummy bit must be exchanged: The 'isrealasize' is not related
src/ltm.h:** the table is using the dummy node; bit 7 is used for 'isrealasize'.)
```

Rett Berg

unread,
Apr 9, 2026, 12:01:09 PM (4 days ago) Apr 9
to lua-l
> __index and ___newindex are called only for not-found key.
Why this non-existence criteria? Why not throw them away in 2026 and
add read and write wrappers for table slots? Like OnRead(), OnWrite()
(or __read, __write if you like underscores)?

I believe the reason is performance. __update would require the write path to _always_ check for the metamethod even if the item was there, whereas __newindex only requires checking it in the nil case, which is more rare for common well-formed tables.

So in the current common case the write path looks like this: 

find slot
if(slot filled) put in slot
else {
  find newindex
  if(has newindex) __newindex(self, k, v)
  else put in slot
}

Update would require the "find update" to be moved up, which would reduce performance for the common case I mentioned.

However, I've been wondering lately whether metatables (do? could?) have some kind of performance enhancement where metamethods are actually stored in a side-struct, avoiding a hash table lookup... or similar. This would still have a performance cost for update, but it would be less.

Just some thoughts,
- Rett


Martin Eden

unread,
Apr 9, 2026, 1:09:29 PM (4 days ago) Apr 9
to lu...@googlegroups.com
On 2026-04-09 18:01, Rett Berg wrote:
>> __index and ___newindex are called only for not-found key.
> Why this non-existence criteria? Why not throw them away in 2026 and
> add read and write wrappers for table slots? Like OnRead(), OnWrite()
> (or __read, __write if you like underscores)?
>
> I*believe* the reason is performance. __update would require the write
> path to_always_ check for the metamethod even if the item was there,
> whereas __newindex only requires checking it in the nil case, which is more
> rare for common well-formed tables.
>
> So in the current common case the write path looks like this:
>
> find slot
> if(slot filled) put in slot
> else {
> find newindex
> if(has newindex) __newindex(self, k, v)
> else put in slot
> }
>
> Update would require the "find update" to be moved up, which would reduce
> performance for the common case I mentioned.
>
> [...]
> Just some thoughts,
> - Rett
Hello Rett,

There is no sense of talking about performance without use cases.
Yes, __newindex code path is faster without __update. But what outer
code does?

If it just handles allocating new slots then no __update is needed.
We have __newindex. This proposal is "considered harmful for performance".

But what if we want to handle all writes to table say for debugging mode?

Currently we will create empty "proxy" table, store real table in closure,
and use __newindex again. Ugly but works.

With proposed __update we will use real table and use __update and
__newindex. Code duplication but okay.

If __update was proposed to replace __newindex then we will just
use real table and __update. Neat.

-- Martin


Roberto Ierusalimschy

unread,
Apr 9, 2026, 3:34:02 PM (4 days ago) Apr 9
to lu...@googlegroups.com
> It seems that in Lua 5.5, the `isrealasize` is no longer in use, and the
> corresponding comments should be able to be removed.
> src/ltable.c:** dummy bit must be exchanged: The 'isrealasize' is not
> related
> src/ltm.h:** the table is using the dummy node; bit 7 is used for
> 'isrealasize'.)
> ```

Thanks for the feedback. We have done that already:

https://github.com/lua/lua/commit/c4e2c91973fed04e7da940c00c92f10f9eb0f9ec

-- Roberto

Yan

unread,
Apr 10, 2026, 6:40:04 AM (3 days ago) Apr 10
to lua-l
Agreed. With __update in place, there's no need for a 'proxy' table — we can use the real table directly, and reads can access fields from the real table without going through the proxy table's __index metamethod.
Reply all
Reply to author
Forward
0 new messages