Skip to content

feat(vm): populate debug.getinfo name/namewhat from the call site#290

Merged
davydog187 merged 4 commits into
mainfrom
feat/debug-getinfo-name
May 31, 2026
Merged

feat(vm): populate debug.getinfo name/namewhat from the call site#290
davydog187 merged 4 commits into
mainfrom
feat/debug-getinfo-name

Conversation

@davydog187
Copy link
Copy Markdown
Contributor

Goal

Make debug.getinfo(level, "n") return a non-nil name and a matching namewhat, so the issue repro passes:

function F(a) return a end
F(1)
assert(debug.getinfo(1, "n").name == 'F')

Success criteria

  • debug.getinfo(1, "n").name == "F" for function F() ... end; F()
  • namewhat reports global / local / upvalue / field / method matching the call site, and "" when no name is known
  • Regression unit test pins the issue repro
  • constructs.lua skip narrows past the debug.getinfo block (was 225..313, now 232..313)
  • mix test and the lua53 suite stay green

Changes

The caller's call instruction already carries a compile-time name hint ({:global, "F"}, {:local, "x"}, ...) — the same hint that powers attempt to call a nil value (global 'foo') errors. Two gaps kept it from reaching debug.getinfo:

  • The interpreter's :call handler pushed a call_stack frame for :lua_closure callees but routed :compiled_closure callees straight into Dispatcher.execute without recording a frame. A top-level function F() ... end; F() therefore ran with an empty call_stack, so getinfo had no name to report.
  • call_stack frames stored only the textual name, not the hint tag needed to classify namewhat.

This PR:

  • Records a call_stack frame for compiled-closure calls in the interpreter (mirroring the existing :lua_closure push), so the running function's name is always available.
  • Threads the hint tag through to a new namewhat field on each frame, derived by hint_namewhat/1 in Lua.VM.Executor.
  • Reads the running function's frame (the head of call_stack when a native callback executes) in Lua.VM.Stdlib.Debug to surface name/namewhat.

No prototype/codegen change was needed: the call-site hint already lives on the instruction stream, which is the faithful PUC-Lua getfuncname model.

Verification

mix format
mix compile --warnings-as-errors   # clean
mix test                           # 2040 passed, 22 skipped (was 2037)
mix test test/lua53_suite_test.exs --only lua53   # 14 passed, 15 skipped

Out of scope

  • The full PUC-Lua getfuncname runtime instruction walk for every form. We reuse the existing compile-time hint, which covers global / local / upvalue / field / method call sites.
  • Naming of functions reached through a call site that carries no hint (call through a temporary, (t[expr])(), immediately-invoked anonymous closures) and functions reached by a tail call: these keep name == nil / namewhat == "", matching PUC-Lua's unknown fallback.
  • name/namewhat for the entry chunk and for native (C) functions inspected by value (debug.getinfo(fn, "n")).

Closes #279

@davydog187
Copy link
Copy Markdown
Contributor Author

Review: feat(vm): populate debug.getinfo name/namewhat from the call site

Reviewed for correctness, tests, conventions, and simplification. Verdict: LGTM, ship it. No blockers.

What I verified

  • Regression tests genuinely pin the bug. Ran the three new debug_test.exs cases against the pre-fix source (main's debug.ex + executor.ex): 3 of them fail (name == nil / all-nil namewhat), and they pass on the branch. The "no hint" case correctly passes both ways since it asserts nil / "".
  • Level-to-frame indexing is correct. Name uses call_stack[level-1] while position uses call_stack[level-2] — these legitimately differ because a frame stores the callee's name (from the call instruction's name_hint) but the caller's line/source. Walked chunk→A→B→getinfo by hand; level 1/2/3 all resolve to the right name, namewhat, and currentline.
  • The compiled-closure frame push is sound. Dispatcher.do_execute_top does not push a frame or bump call_depth on entry — it relies on callers to do so, exactly as the dispatcher's own internal call sites (push → execute → pop) do. The interpreter's :call handler previously skipped this for :compiled_closure, so the fix also (correctly) makes the interpreter→dispatcher hop respect check_call_depth!. Pop ordering mirrors the existing sites.
  • Full validation green: mix format --check-formatted, mix compile --force --warnings-as-errors (clean), mix test (2040 passed, 22 skipped), mix test --only lua53 (14 passed, 15 skipped).
  • Skip narrowing is minimal and real. constructs.lua 225..313 → 232..313 un-skips the debug.getinfo(1,"n").name == 'F' block (lines 225-231), which now runs as part of the suite and passes. Reason string dropped the stale debug.getinfo mention; issue field stays nil.

Conventions: all clean

  • Title/feat scope is vm (allowed subsystem), not a plan id. chore(A44) commits touch only the plan file, which is the documented exception.
  • No plan-id references in any source/test the PR adds.
  • No Co-Authored-By trailers.
  • Body has Closes #279.

Minor (non-blocking)

  • hint_namewhat/1 has 2-tuple clauses {:field, _} and {:method, _}, but the compiler's name_hint/2 only ever emits the 3-tuple forms {:field, name, recv} / {:method, name, recv}. The 2-tuple clauses are dead, though they harmlessly mirror the defensive style in format_target_hint/1.
  • The value-based path (debug.getinfo(somefunc, "n")) still omits namewhat entirely, so it reads as nil in Lua rather than PUC-Lua's "". Pre-existing and explicitly out of scope here.

debug.getinfo(level, "n") previously hardcoded name == nil and never set
namewhat. The caller's call instruction already carries a compile-time
name hint ({:global, "F"}, {:local, "x"}, ...) — the same hint that powers
"attempt to call a nil value (global 'foo')" errors — but two gaps kept it
from reaching debug.getinfo:

- The interpreter's :call handler pushed a call_stack frame for :lua_closure
  callees but routed :compiled_closure callees straight into Dispatcher.execute
  without recording a frame. A top-level `function F() ... end; F()` therefore
  ran with an empty call_stack, so getinfo had no name to report.
- call_stack frames stored only the textual name, not the hint tag needed to
  classify namewhat.

Now the interpreter records a frame for compiled-closure calls too, frames
carry namewhat derived from the hint tag, and debug.getinfo reads the running
function's frame (the head of call_stack when a native callback executes) to
surface name/namewhat. Covers the global, local, upvalue, field, and method
call forms; calls with no hint (through a temporary, tail calls) report
name == nil / namewhat == "", matching PUC-Lua's unknown fallback.

Narrows the constructs.lua suite skip past the debug.getinfo block.

Plan: A44
Closes #279
The compiler's name_hint/2 only emits 3-tuple {:field, name, recv} and
{:method, name, recv} forms, so the 2-tuple {:field, _} / {:method, _}
clauses in hint_namewhat/1 were unreachable. The trailing catch-all still
handles any unexpected shape defensively.
@davydog187 davydog187 force-pushed the feat/debug-getinfo-name branch from 70dbf0f to aef0f5c Compare May 31, 2026 12:13
@davydog187 davydog187 merged commit cd3b48e into main May 31, 2026
5 checks passed
@davydog187 davydog187 deleted the feat/debug-getinfo-name branch May 31, 2026 12:33
davydog187 added a commit that referenced this pull request Jun 1, 2026
Merge main and rebuild the constructs.lua triage on top of it. The os
stdlib (#289) and debug.getinfo name resolution (#290) both landed, so
the debug.getinfo block (line 226), the os.time assignment (line 237),
and the GLOB1 concat (line 248) all pass now and no longer need skipping.

Empirically re-triaged constructs.lua against the current tree: the only
remaining failures are the short-circuit harness (284..299, level=4
combination explosion exceeds the test timeout) and the checkload block
(302..311, load() error messages do not contain the expected
'expected'/'too long' substrings). Replace the single 232..313 entry
with these two narrowed, disjoint ranges.

Drops the duplicate A43-os-stdlib.md plan (the os work shipped via
A43-os-library.md / #289) and the debug.getinfo name pinning test
(superseded by the tests #290 shipped on main).

Plan: A26
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.

debug.getinfo(level, 'n') returns nil for name and namewhat

1 participant