Skip to content

types, vm, lib: add metamethod support for prototyped values - #434

Open
jow- wants to merge 1 commit into
masterfrom
feat/metamethods
Open

types, vm, lib: add metamethod support for prototyped values#434
jow- wants to merge 1 commit into
masterfrom
feat/metamethods

Conversation

@jow-

@jow- jow- commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Add get, set, delete, call and tostring metamethods for values carrying a prototype chain. Metamethods are resolved at runtime from the contents of that chain, no new bytecode and no new VM API are involved.

The semantics follow Lua's __index / __newindex convention:

get(key) fires on a read which neither the value nor its
prototype chain answers; returning a table delegates
the lookup to that table
set(key, val) fires on writing a key an object does not have yet;
its return value is discarded, an assignment always
evaluates to the assigned value
delete(key) fires on deleting a key an object does not have yet;
a truthy result means "deleted"
call(...) makes the value invocable, through uc_vm_call() and
ucv_is_callable() alike
tostring generalizes the existing "tostring" convention, the
legacy name stays valid as an alias

Own keys and numeric array indices always win, metamethods are the fallback: object literals store their members without dispatching, and arrays keep their dense storage, so a key which is a valid index never dispatches. The in operator walks the prototype chain for keys but does not dispatch, a purely virtual property stays hidden.

ucv_metamethod_lookup() walks the prototype chain starting at the direct prototype and accepts a function only: an own property with a dunder name is plain data, so ordinary property writes cannot accidentally install a metamethod, and a value holding itself as call cannot report itself as its own metamethod and recurse through C. No other re-entrancy guard exists: a metamethod re-entering the operation it customizes runs into the VM's call depth limit, which raises "Too much recursion".

The dispatch lives in one place, ucv_key_get(), ucv_key_set() and ucv_key_delete() in types.c, called directly by the instruction handlers for property loads, stores, updates and deletes. Their shared call helper pushes receiver, metamethod, key and value on the VM stack, keeping them reachable for a garbage collection running inside the metamethod, and restores the stack when the call raises. An exception raised by a metamethod propagates to the interpreter loop; uc_vm_insn_update_val() saves the arithmetic opcode before dispatching and skips the write half when the read half raised. ucv_object_add() and ucv_array_set() take ownership of the value stored but leave it with the caller where they reject it, which the raw helpers account for so that a rejected store does not leak.

rawget(), rawset() and rawdelete() reach the storage below a metamethod and are exposed to scripts, and as ucv_key_rawget(), ucv_key_rawset() and ucv_key_rawdelete() to C modules.

Add tests/custom/06_metamethods covering the five metamethods, own key and index precedence, the raw accessors, exception propagation and the recursion limit, and document the feature as the official docs/tutorials/06-metamethods.md article.

Add __get__, __set__, __delete__, __call__ and __tostring__ metamethods
for values carrying a prototype chain. Metamethods are resolved at
runtime from the contents of that chain, no new bytecode and no new VM
API are involved.

The semantics follow Lua's __index / __newindex convention:

  __get__(key)      fires on a read which neither the value nor its
                    prototype chain answers; returning a table delegates
                    the lookup to that table
  __set__(key, val) fires on writing a key an object does not have yet;
                    its return value is discarded, an assignment always
                    evaluates to the assigned value
  __delete__(key)   fires on deleting a key an object does not have yet;
                    a truthy result means "deleted"
  __call__(...)     makes the value invocable, through uc_vm_call() and
                    ucv_is_callable() alike
  __tostring__      generalizes the existing "tostring" convention, the
                    legacy name stays valid as an alias

Own keys and numeric array indices always win, metamethods are the
fallback: object literals store their members without dispatching, and
arrays keep their dense storage, so a key which is a valid index never
dispatches. The `in` operator walks the prototype chain for keys but
does not dispatch, a purely virtual property stays hidden.

ucv_metamethod_lookup() walks the prototype chain starting at the
direct prototype and accepts a function only: an own property with a
dunder name is plain data, so ordinary property writes cannot
accidentally install a metamethod, and a value holding itself as
__call__ cannot report itself as its own metamethod and recurse
through C. No other re-entrancy guard exists: a metamethod re-entering
the operation it customizes runs into the VM's call depth limit, which
raises "Too much recursion".

The dispatch lives in one place, ucv_key_get(), ucv_key_set() and
ucv_key_delete() in types.c, called directly by the instruction handlers
for property loads, stores, updates and deletes. Their shared call helper
pushes receiver, metamethod, key and value on the VM stack, keeping them
reachable for a garbage collection running inside the metamethod, and
restores the stack when the call raises. An exception raised by a
metamethod propagates to the interpreter loop; uc_vm_insn_update_val()
saves the arithmetic opcode before dispatching and skips the write half
when the read half raised. ucv_object_add() and ucv_array_set() take
ownership of the value stored but leave it with the caller where they
reject it, which the raw helpers account for so that a rejected store
does not leak.

rawget(), rawset() and rawdelete() reach the storage below a metamethod
and are exposed to scripts, and as ucv_key_rawget(), ucv_key_rawset()
and ucv_key_rawdelete() to C modules.

Add tests/custom/06_metamethods covering the five metamethods, own key
and index precedence, the raw accessors, exception propagation and the
recursion limit, and document the feature as the official
docs/tutorials/06-metamethods.md article.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant