Skip to content

test: document Luerl reference semantics for ao.unset #869

Open
Lucifer0x17 wants to merge 3 commits intoedgefrom
test/luerl-table-reference-semantics
Open

test: document Luerl reference semantics for ao.unset #869
Lucifer0x17 wants to merge 3 commits intoedgefrom
test/luerl-table-reference-semantics

Conversation

@Lucifer0x17
Copy link
Copy Markdown
Collaborator

#829 introduced the ao.unset sentinel for removing fields from patch@1.0 cached state. It added convert_unset_values/1 at the HB boundary so "__ao-unset__" strings get converted to the unset atom that dev_message:set/3 treats as a key removal (which in itself was a bug). But the fix only works, till the "__ao-unset__" reaches to HB. Found out, it doesn't always.

The remaining foot-gun

ao.send or Send() shallow-clones top-level keys of the message. When a handler does:

  Routes[owner] = ao.unset
  Send({ device = "patch@1.0", routes = Routes })
  Routes[owner] = nil  

...the outbox entry's routes field is a #tref pointing to the same Luerl heap table as the global Routes. The post-Send, Routes[owner] = nil calls ttdict:erase on the shared table. By the time the Lua func returns and luerl:call_function_dec walks the heap to decode the outbox, the sentinel key is gone. convert_unset_values never sees it. patch@1.0 device never learns the key should be removed.

The pattern that works

Use a fresh small table and do not mutate the source between Send & the decode boundary.

-- Bad: reference leak, nil erases sentinel before patch@1.0 sees it

Routes[owner] = ao.unset
Send({ device = "patch@1.0", routes = Routes })                                                                            
Routes[owner] = nil                                                                                                      

-- Good: targeted small table, no shared ref to mutate

Routes[owner] = nil
Send({ device = "patch@1.0", routes = {[owner] = ao.unset} })                                                              

The good pattern is O(1), explicit about intent, and cheaper computation than shallow-cloning the full table inside ao.send (which can be used when there is a multiple update in a single call).

This PR

Adds ao_unset_reference_bug_test in dev_lua.erl with a companion Lua func in test/test.lua. Two assertions document both semantics:

  • shared/wallet1 -> not_found (reference leak erased the sentinel)
  • targeted/wallet1 -> "ao-unset" (since, it's kinda new table, it survives)

Locks in the Luerl reference behavior as a contract and gives future lua process authors an executable reference for the right pattern.

Lucifer0x17 and others added 3 commits April 17, 2026 18:38
egression test for the patch@1.0 sentinel pattern. Demonstrates that passing a full table into an outbox-like container and then setting a key to nil erases it from both sides.
Hence, there is an erratic behaviour in the patched state.
egression test for the patch@1.0 sentinel pattern. Demonstrates that passing a full table into an outbox-like container and then setting a key to nil erases it from both sides.
Hence, there is an erratic behaviour in the patched state.

Co-authored-by: Jack Frain <jfrain99@gmail.com>
…tics' into test/luerl-table-reference-semantics
@Lucifer0x17 Lucifer0x17 self-assigned this Apr 17, 2026
@Lucifer0x17 Lucifer0x17 requested a review from jfrain99 April 17, 2026 22:59
@Lucifer0x17 Lucifer0x17 added bug Something isn't working documentation Improvements or additions to documentation labels Apr 17, 2026
@samcamwilliams
Copy link
Copy Markdown
Collaborator

Interesting, but I am confused! I thought the idea here was that the sending mechanics from ~genesis-wasm@1.0 Lua to HB have difficulty when the patched message has a nil in it. That makes sense because nil gets stripped by the Erlang-side JSON parser, IIRC.

If it is ~lua@5.3a why are we using ~patch@1.0 at all? We should just be able to set the keys on the state message before returning it, like all of the tests in the Lua module? I am not certain of how Tom did this for Luerl AOS though.

@Lucifer0x17
Copy link
Copy Markdown
Collaborator Author

If it is ~lua@5.3a why are we using ~patch@1.0 at all?

No, we are not. The test just isolates the behaviour. It imitates the same values that ~genesis-wasm@1.0 would return, written this way so the Luerl reference semantics are isolated from everything else. The same behaviour manifests with ~genesis-wasm@1.0 as the execution device — the erasure happens at the Luerl heap level before either path crosses to Erlang, so whether the read is luerl:call_function_dec (here) or JSON-encode-then-dev_json_iface (in ~genesis-wasm@1.0), the key is already gone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants