feat(debug): implement debug.sethook/gethook (line + count events) - #34
Open
xepozz wants to merge 1 commit into
Open
feat(debug): implement debug.sethook/gethook (line + count events)#34xepozz wants to merge 1 commit into
xepozz wants to merge 1 commit into
Conversation
xepozz
force-pushed
the
feature/debug-line-count-hook
branch
from
August 3, 2026 17:20
9dda1e6 to
b9863a6
Compare
Adds line and count debug hooks, the primitive test-coverage tooling
(LuaCov-style, LCOV output) is built on. debug.sethook/gethook were already
declared in the type stub (compiler/stdlib/debug.go) but had no runtime
implementation, so any coverage instrumentation was impossible.
- hook.go: sethook/gethook + the callHook/fireHook engine. All state lives on
LState (hook, hookMask, hookCount, hookCounter, hookLastLine, inHook) — the
hot callFrame struct is left untouched, so there is no per-frame size cost on
the call path.
- vm.go: one guarded call at the dispatch-loop head; when no hook is installed
it is a single uint8 compare. Measured (benchstat, n=10, paired vs main):
every VM benchmark within noise, geomean -0.10% — no measurable overhead.
- Line events fire when the current source line changes (tracked on LState).
This is a coverage-oriented simplification of PUC's per-frame oldpc tracking:
every executed line is reported at least once; it does not re-fire a line on
a same-frame loop back-edge. Happy to switch to full per-frame semantics if
preferred.
- Reentrancy is blocked via LState.inHook so a hook cannot trigger itself; the
register top is saved/restored around the call.
- Call/return ('c'/'r') masks are rejected explicitly rather than silently
ignored — line/count only for now.
hook_test.go covers coverage collection, count firing, gethook round-trip,
nested calls, reentrancy, sethook(nil) clearing, and c/r rejection.
Full suite (2199 tests) green, go vet clean. No generated files touched.
xepozz
force-pushed
the
feature/debug-line-count-hook
branch
from
August 3, 2026 18:02
b9863a6 to
b1454fc
Compare
This was referenced Aug 4, 2026
Contributor
|
this would not work under v2 semantics... could be the same achieved without lua impact? |
Author
|
I'm ok to have it now for v1 and change/remove later. I'd think about capabilities of v2 when v2 will be published. |
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.
What
Implements the runtime side of
debug.sethook/debug.gethook, supporting line and count events. These functions were already declared in the type stub (compiler/stdlib/debug.go) but had no runtime implementation, sodebug.sethookdid not exist on anLStateat runtime.Why
Line hooks are the primitive every Lua test-coverage tool is built on (LuaCov and friends register a line hook and record executed lines, then emit an LCOV tracefile). Without
sethookthere is no way to measure coverage of Lua code running on this VM. This PR adds the minimum needed to enable that. (I have a working LuaCov-style collector + LCOV emitter verified against this branch — happy to share as an example if useful.)Design
hook.go(new) — the whole feature: mask constants,callHook/fireHook,debugSetHook/debugGetHook.LState(hook,hookMask,hookCount,hookCounter,hookLastLine,inHook). The hot, pooledcallFramestruct is left untouched — no per-frame size growth, so the call path pays nothing. No generated files are modified.vm.go— one guarded call at the dispatch-loop head, after the yield-continuation check so it only fires on normal instruction dispatch:hookLastLineonLState). This is a deliberate, coverage-oriented simplification of PUC's per-frameoldpctracking: every executed line is reported at least once; it does not re-fire a line on a same-frame loop back-edge, and same-line tracking is global rather than per-CallInfo. If you'd prefer full per-frame PUC semantics I'm glad to movehookLastLineinto a per-frame slot.LState.inHook— a hook cannot trigger itself; the register top is saved/restored so the hook is transparent to the interrupted instruction.c/r(call/return) masks are rejected explicitly with an error rather than silently ignored. Line+count is what coverage needs; call/return can be a follow-up.Performance
The dispatch-loop guard is a single
uint8compare when no hook is installed. Measured withbenchstat(n=10, paired againstmain) across the VM hot-path benchmarks (ForNumeric,ArithIntOnly,LocalAccess,VMFunctionCall,ForPairs,Comparison):No measurable overhead for runs that do not install a hook.
Semantics
debug.sethook(fn, mask [, count])—maskmay containl;count > 0enables the count hook.debug.sethook()/debug.sethook(nil)clears.fn(event, line)—eventis"line"or"count";lineis the current line for"line",nilotherwise.debug.gethook()returns(fn, maskstring, count).Tests
hook_test.go(new) covers: coverage collection via a line hook, count-hook firing,gethookround-trip, line events across nested calls, reentrancy safety,sethook(nil)clearing, andc/rrejection.