feat(vm): inspect protocol for VM values via boundary display structs#218
Merged
Conversation
Tighten Success criteria, Implementation notes, and Risks to remove
ambiguity about which value tags get wrapped in which decode mode.
The wrap layer leaves `decode: true` table/userdata shape unchanged
(preserving the existing list-of-tuples and `{:userdata, term}` API
that all doctests and `deflua` flows depend on), and only wraps
closures/native_funcs in default decode mode (where they already
leak as raw tuples today). Decode: false wraps all four kinds.
Module names move under `Lua.VM.Display.*` to avoid collision with
the existing `Lua.Table` public utility module.
Plan: A27
Wrap opaque VM tags returned from `Lua.eval/2` and `Lua.eval!/2` in
display structs so iex shows something legible instead of raw tuples
like `{:tref, 7}` or `{:lua_closure, _, _}`. Internal pattern matches
on those tags are unaffected because the wrap fires at the eval
boundary only.
Decode-mode matrix:
| tag | decode: true | decode: false |
|----------------------|-----------------------------|-----------------------------|
| {:tref, _} | unchanged (list of tuples) | %Lua.VM.Display.Table{} |
| {:udref, _} | unchanged ({:userdata, t}) | %Lua.VM.Display.Userdata{} |
| {:lua_closure, _, _} | %Lua.VM.Display.Closure{} | %Lua.VM.Display.Closure{} |
| {:native_func, _} | %Lua.VM.Display.NativeFunc{}| %Lua.VM.Display.NativeFunc{}|
Adds a public `Lua.unwrap/1` helper for callers that need the raw
VM tag (e.g. to round-trip back through internal helpers), and
teaches `Lua.set!/3`, `Lua.encode!/2`, `Lua.decode!/2`, and
`Lua.call_function/3` to unwrap display structs at their entry
points so the wrapped values round-trip seamlessly.
Default-decode tables and userdata keep their existing list-of-tuples
and `{:userdata, term}` shapes, so deflua flows and existing
doctests are unaffected. Two pre-existing tests that pattern-matched
on `{:tref, _}` from `decode: false` were updated to match the new
`%Lua.VM.Display.Table{}` shape (and exercise `Lua.unwrap/1`).
Plan: A27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Inspect protocol for VM values
Plan:
.agents/plans/A27-inspect-protocol.mdGoal
Implement
Inspectfor the four opaque VM value tags soiexshowssomething useful instead of a tuple soup. Today, a
{:tref, 7}inthe iex output tells the user nothing about what's in the table; a
{:lua_closure, _, _}tells them nothing about the function.Targets:
{:tref, integer()}— table reference. Renders contents (ora short summary if large) and the table id.
{:lua_closure, _proto, _upvalues}— Lua function. Renderssource location and arity (with
+...for varargs).{:native_func, fun}— Elixir-defined Lua callable. Rendersmodule/function/arity via the function's own inspect output.
{:udref, integer()}— userdata reference. Renders theunderlying Elixir term and the udref id.
Approach: boundary wrap
When a Lua value crosses out to Elixir (
Lua.eval/2/Lua.eval!/2return path), wrap each VM tag in a thin display struct. The struct
exists only for outbound display; internally the VM still uses the
tagged-tuple form, and pattern matches against
{:tref, _}etc.inside the VM are unaffected.
Module layout:
Lua.VM.Display.Table—[:id, :peek, :ref]Lua.VM.Display.Closure—[:source, :line, :arity, :vararg?, :ref]Lua.VM.Display.NativeFunc—[:fun, :ref]Lua.VM.Display.Userdata—[:id, :term, :ref]The display structs each carry the original VM tag tuple on
:ref, socallers can round-trip the value back into the VM.
Lua.set!/3,Lua.encode!/2,Lua.decode!/2, andLua.call_function/3allunwrap display structs at their entry points so wrapped values flow
back through the public API without ceremony. A new public
Lua.unwrap/1helper exposes the underlying tuple for any callerthat needs it directly.
Decode-mode matrix
decode: true(default)decode: false{:tref, _}%Lua.VM.Display.Table{}{:udref, _}{:userdata, term})%Lua.VM.Display.Userdata{}{:lua_closure, _, _}%Lua.VM.Display.Closure{}%Lua.VM.Display.Closure{}{:native_func, _}%Lua.VM.Display.NativeFunc{}%Lua.VM.Display.NativeFunc{}Default-decode tables and userdata keep their existing
Elixir-friendly shapes (list of
{k, v}tuples and{:userdata, term}) sodefluaflows and existing doctests areunaffected. Closures and native funcs are wrapped in both modes
because their default decoded form has always been the raw tuple.
Two pre-existing tests pattern-matching on
{:tref, _}fromdecode: falsewere updated to match the new%Lua.VM.Display.Table{}shape (and exercise
Lua.unwrap/1).Success criteria
Lua.eval!(Lua.new(), "return {1, 2, 3}", decode: false)shows#Lua.Table<id: 11, [1, 2, 3]>instead of{:tref, 11}. Verifiedmanually and in
test/lua/vm/display_test.exs.#Lua.Closure<source: "<eval>", line: 1, arity: 2>(and
arity: N+...for variadics) in both decode modes.#Lua.NativeFunc<#Function<...>>in bothdecode modes.
Lua.eval!(lua, "return opaque", decode: false)for a userdatashows
#Lua.Userdata<id: 0, term: %{secret: 42}>.Inspect.Opts— large table peekstruncate to
:limit, andto_doc/2is used for nested values.list-of-tuples;
{:userdata, term}is preserved.mix testpasses (1654 tests, 0 failures, +28 fromtest/lua/vm/display_test.exs).Manual verification
Changes
Verification
Discoveries
Lua.Tablealready exists as a public utility module (for castingdecoded tables to lists/maps). Display structs live under
Lua.VM.Display.*to make the namespace clear and avoid collisions.Inspect output still uses the short forms (
#Lua.Table<...>,#Lua.Closure<...>, etc.) regardless of full module path.Lua.Compiler.Prototypestruct carries:source,:lines(a{first, last}tuple),:param_count, and:is_vararg— thedisplay wrap reads from those four fields, no proto changes needed.
Lua.set!/3,Lua.encode!/2, etc.) had to beadded to keep the wrap transparent for existing flows. The cleanest
fix was a single
Display.unwrap/1call at each public-API entrypoint, plus an opportunistic
Util.encoded?/1shortcut inencode!to handle the case where the unwrapped value is already a raw VM tag.
Out of scope (intentional)
affordance, not a data-model change.
Inspect.Opts.limittruncation. The peek caps at
:limitentries and shows…forthe rest.
Lua.dbg/2helper — that's plan A28.decode: truemode (would change theexisting
{:userdata, term}API; out of scope by design).