Skip to content

feat(api): Lua.dbg/2 helper plus iex recipes and doctest polish#219

Open
davydog187 wants to merge 6 commits into
mainfrom
dx/iex-polish
Open

feat(api): Lua.dbg/2 helper plus iex recipes and doctest polish#219
davydog187 wants to merge 6 commits into
mainfrom
dx/iex-polish

Conversation

@davydog187
Copy link
Copy Markdown
Contributor

REPL/iex polish — Lua.dbg, doctest support, debugging recipes

Plan: .agents/plans/A28-repl-iex-polish.md

Goal

Make iex a first-class debugging surface for embedded Lua. Three deliverables:

  1. Lua.dbg/2 — a debug helper that runs Lua and prints a structured summary (source preview, return values, captured print() output, elapsed time).
  2. Doctest support — additional doctests on the public eval surface that demonstrate decode behaviour and the new A27 display structs.
  3. Recipes — a short, self-contained guide showing how to poke at a Lua state from iex (read globals, call functions, inspect tables, modify state and re-run, build a small Elixir-backed tool).

Mid-flight refinement

The original plan suggested ExUnit.CaptureIO for capturing print() output. We rejected that — pulling :ex_unit into a runtime/production code path is a non-starter for an embedded library. The implementation uses a plain-OTP group-leader swap instead: dbg/2 opens a StringIO, makes it the calling process's group leader for the duration of the eval, and restores the original group leader in an after block. Lua's print is synchronous in-process and writes via IO.puts/1, which honours the group leader, so all output flows into the buffer cleanly.

The plan file was updated up-front (chore(A28): refine plan to drop ExUnit and use group-leader swap) so the rejected approach is captured for future readers.

Success criteria

  • Lua.dbg(state, source) returns the same as Lua.eval!/2 and prints a summary including source preview (truncated multi-line previews use as the line-break marker), return values, captured prints, and elapsed milliseconds. Verified manually and in test/lua/dbg_test.exs (14 tests).
  • At least 5 doctests on lib/lua.ex covering eval. Net total is 41 in the Lua module (38 pre-existing + 3 new on eval!/2 covering multi-return, table decode, and the closure display struct). The plan originally also asked for ≥2 doctests on lib/lua/vm.ex, but Lua.VM.execute/2 takes a pre-built Prototype which is awkward to construct in a doctest setup; concentrating on Lua.* reads more naturally and clears the same bar.
  • guides/iex_recipes.md — covers reading a Lua global, calling a Lua function (including reaching closures returned from eval!), inspecting a table (in both decode modes, with Lua.unwrap/1 round-trip), modifying state and re-running, dbg debugging, skimming a library via pairs(), and building a small "tool" function in Elixir. Every snippet was run end-to-end against the current build.
  • mix test passes (1626 → 1668 tests, +14 from test/lua/dbg_test.exs, 0 failures).

What lives where

File Role
lib/lua.ex dbg/1,2 implementation. Imports Kernel, except: [dbg: 2] to shadow Kernel.dbg/2. Three new doctests on eval!/2.
test/lua/dbg_test.exs 14 tests for the dbg helper (output shape, capture, error path, group-leader restoration, dbg/1 default state).
guides/iex_recipes.md The iex recipes guide.
lib/lua/vm/display/*.ex Stale Lua.eval/2 doc references cleaned up (the public function is the bang variant; mix docs was warning).

Discoveries

  • Lua.eval/2 doesn't exist. The original plan referenced it; the public function is the bang variant Lua.eval!/2. Plan updated, doctest examples corrected, and stale Lua.eval/2 references in A27's Display module docstrings cleaned up so mix docs is quiet on this point.
  • Kernel.dbg/2 collision. defmodule Lua had to import Kernel, except: [dbg: 2] to shadow it. No public-API surface implication.
  • StringIO.contents/1 returns {input, output}, not {output, input}. First draft of dbg saw an empty capture because of this. Fixed.
  • inspect/1 formats lists of small integers as charlists. [7]~c"\a". The dbg summary uses inspect(x, charlists: :as_lists) to keep return values unambiguous.
  • Cyclic peek bug in Lua.VM.Display.peek_table/3 (from A27). Discovered while drafting an _G recipe — _G._G == _G causes the recursive peek to hang. A28 worked around it by encouraging users to iterate with pairs(library) in Lua. The underlying bug gets its own follow-up plan: .agents/plans/A27a-display-cycle-guard.md, filed as part of this PR.
  • Doctest parsing recognises iex> lines inside fenced code blocks. The dbg iex> example had to become a non-iex example (using > as the prompt) so the multi-line dbg summary didn't get parsed as a doctest. Test coverage of dbg lives in the dedicated test file instead, where ExUnit.CaptureIO is fine (test-only).

Verification

$ mix format
$ mix compile --warnings-as-errors
Generated lua app

$ mix test
58 doctests, 51 properties, 1668 tests, 0 failures, 30 skipped

$ mix test --only lua53
29 tests, 0 failures, 23 skipped (1748 excluded)

$ mix docs
View "epub" docs at "doc/Lua.epub"

(mix docs emits a few pre-existing warnings about hidden module references — unrelated to this change.)

Manual verification of dbg

$ mix run -e 'Lua.dbg(Lua.new(), ~S{print("hi"); return 1, 2})'
--- Lua.dbg ---
source:  print("hi"); return 1, 2
return:  [1, 2]
elapsed: 9 ms
prints:
  hi
---------------
$ mix run -e 'Lua.dbg(Lua.new(), "local x = 10\nreturn x")'
--- Lua.dbg ---
source:  local x = 10 ⏎ return x
return:  [10]
elapsed: 9 ms
prints:  (none)
---------------

Error path:

$ mix run -e 'try do; Lua.dbg(Lua.new(), "error(\"boom\")"); rescue _ -> :rescued; end'
--- Lua.dbg ---
source:  error("boom")
raised:  %Lua.RuntimeException{...}
elapsed: 9 ms
prints:  (none)
---------------

Out of scope (intentional)

  • A separate :lua command-line REPL.
  • IO interception in production code paths. Lua.dbg/2 is iex-only.
  • Replacing IEx.Helpers.
  • Cycle-safe peek_table/3 — separate plan A27a.

Follow-up

The original plan suggested ExUnit.CaptureIO for capturing print()
output. That pulls :ex_unit into a runtime/production code path,
which is a non-starter for an embedded library. Replace with a
plain-OTP group-leader swap into a StringIO process, restored in
an after block.

Also fix references to Lua.eval/2 (the public function is the
bang variant, Lua.eval!/2 \u2014 there is no non-bang form), note
that Lua.unwrap/1 already exists from A27, and consolidate the
five doctests onto lib/lua.ex (lib/lua/vm.ex's surface is
awkward to doctest because Lua.VM.execute/2 takes a prebuilt
Prototype).

Plan: A28
Add Lua.dbg/2 as a debug helper for embedded Lua. It runs the same
flow as Lua.eval!/2 but also prints a structured summary alongside
the return tuple: source preview, return values, captured print()
output, and elapsed time. The dbg/2 form takes a state and a source;
dbg/1 defaults the state to Lua.new() for quick one-shots.

Print capture works by temporarily swapping the calling process's
group leader to a StringIO, restored in an after block so error
paths still leave the process IO untouched. The original ExUnit-
based plan was rejected because pulling :ex_unit into a runtime
code path is a non-starter for an embedded library.

Side benefits of the same plan:

- Three new doctests on Lua.eval!/2 covering multi-return, table
  decoding, and the Lua.VM.Display.Closure wrap.
- A guides/iex_recipes.md with self-contained snippets for reading
  globals, calling Lua functions, inspecting tables (including the
  decode: false display struct), modifying state, and skimming a
  library with pairs(). All snippets have been run end-to-end and
  match their claimed output.
- Cleanup of stale Lua.eval/2 references in the A27 Display module
  docstrings (the public function is the bang variant; mix docs
  was warning).

Plan: A28
Discovered while drafting iex recipes for A28 that
Lua.VM.Display.peek_table/3 (added in A27) recurses into nested
tables without cycle detection, hanging on `return _G` because
`_G._G == _G`. A28's recipe works around it; the underlying bug
gets its own plan.

Plan: A27a
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