lvm.c holds 3 of the 5 highest-risk functions in lua/lua, and that concentration matters more than any single score: luaV_equalobj, luaV_execute and luaV_finishOp all sit in the same module, all are in the critical band, and all were last changed 3 days ago. The top function, luaV_equalobj, has an activity-weighted risk score of 2.25 and is in the active tier with 10 touches in the last year, which makes it complex code that is also changing now, not a backlog item. This analysis covers 1,296 functions in Lua’s C codebase, 133 of them critical, and I would start with the interpreter loop and its neighbors before anything else.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
luaV_equalobj | lvm.c | 2.2 | 29 | 5 | 16 |
luaV_execute | lvm.c | 2.2 | 11 | 4 | 120 |
luaK_posfix | lcode.c | 2.1 | 29 | 3 | 19 |
luaV_finishOp | lvm.c | 2.1 | 30 | 2 | 9 |
luaK_finish | lcode.c | 2.1 | 18 | 3 | 10 |
The distribution first. Of the 1,296 functions I analyzed, 890 are in the active tier and 406 are quiet. That is a high share of active code, so the critical band here mostly means live regression surface, not dormant debt.
1,296 functions analyzed
The pattern in the top 5 is plain: lvm.c has three entries and lcode.c has two. These are the virtual machine and the code generator, the two halves of how Lua turns source into executed behavior. All five show 10 or 11 touches in the last year and 3 days since last change. The file-level history around them is mild. None of the five files shows bug-linked commits or reverts, bug-fix rates run from 11.8% to 25%, and each file has a single author in the last 90 days. I read that as a complexity-and-churn story, not a track record of defects. Complexity is simply concentrated in code that is moving.
luaV_equalobj — lvm.c
I would start with luaV_equalobj because it has the highest activity-weighted risk score in the repository (2.25) and sits in code touched 10 times in the last year, most recently 3 days ago. It is the generic equality routine, and the source shows why its numbers are what they are: a type-tag check, then nested switch statements for mixed-variant comparisons (integer vs float, short vs long strings) and for same-variant comparisons, with a metamethod fallback for userdata and tables at the end.
A cyclomatic complexity of 29 with nesting depth 5 and fan-out 16 means a lot of independent paths, each one a test case. The nesting is the part I would watch: the metamethod-lookup branches sit inside a switch inside an else branch, so a reader must hold the earlier type checks in mind to know what state they are in. The file history is quiet (an 11.8% bug-fix rate, no reverts, one author in the last 90 days), so this is maintainability debt in actively changing code, not a flagged problem.
My recommendation: split the two switch statements into separate helpers, one for different-variant comparisons and one for same-variant comparisons, and pull the userdata and table metamethod lookup into a shared helper, since those two cases appear to duplicate the same shape. That would take the main function down to a dispatcher and make each equality rule testable on its own. Because this is a hot, semantically delicate routine, I would only do it with equality tests across every type pair in place first.
luaV_execute — lvm.c
luaV_execute is the interpreter’s main loop, and its profile is the opposite of luaV_equalobj: cyclomatic complexity is only 11 and nesting is 4, but fan-out is 120. That is the highest coupling in the whole top 5 by a wide margin. The excerpt shows a fetch-and-dispatch loop with one case per opcode, plus macros (vmfetch, vmdispatch, vmcase, vmbreak) and an optional jump-table include. In C, that macro layer hides structure that the complexity numbers do not capture.
So the risk here is not branching, it is breadth. With 120 distinct callees, almost any change to a runtime subsystem can touch this function, and the function has 10 touches in the last year, most recently 3 days ago. It shares file-level history with the rest of lvm.c: an 11.8% bug-fix rate, no reverts, no bug-linked commits.
This is a case where I would not recommend extracting the opcode handlers: splitting a dispatch loop like this can have real performance consequences, and the source leans heavily on its macros. My recommendation is narrower. Treat any change to this function as high blast radius, require a full opcode-level test run, and review diffs here against the callees they touch.
luaK_posfix — lcode.c
luaK_posfix is the code generator’s handler for finishing binary operators, and it has the second-highest activity-weighted risk in the list at 2.12 with 11 touches in the last year. The source is a large switch on the operator: logical and/or, concat, commutative and non-commutative arithmetic, bitwise operators, shifts, equality, and ordering. Some cases swap operands or fall through into other cases, such as subtraction into the general arithmetic path and greater-than into less-than.
A cyclomatic complexity of 29 with nesting of 3 and fan-out of 19 puts it in the same complexity range as luaV_equalobj but with shallower structure. The fall-through cases are the part I would flag: they mean a case’s behavior depends on what follows it in the file, so reordering or inserting a case can change semantics silently. The file’s bug-fix rate is 25% across 16 commits, the highest in this top 5, though with no reverts and no bug-linked commits it is context, not evidence about this function.
My recommendation: make the fall-through paths explicit by routing them through named helpers instead of relying on case order, and group the shift, comparison and bitwise cases into their own small functions. Operator emission is exactly the sort of code where a regression changes generated bytecode, so I would add a test that compares emitted instructions for each operator before refactoring.
luaV_finishOp — lvm.c
luaV_finishOp completes an opcode that was interrupted, and the excerpt shows a single flat switch over opcode groups: metamethod binary ops, table gets, comparisons, concat, close, and return, with a default that only asserts which opcodes may yield. A cyclomatic complexity of 30 with nesting of only 2 and fan-out of 9 describes a wide, shallow function. It is easier to read than luaV_equalobj, but each case carries its own assumption about stack state and saved program counter.
That is where I would be careful. Each branch encodes what the interpreter loop left on the stack when it yielded, so a change to how luaV_execute handles any of those opcodes may require a matching edit here. Both functions have 10 touches in the last year and changed 3 days ago, which makes that coupling worth checking in review.
My recommendation: add a comment-level or test-level table mapping each yieldable opcode to its resume behavior in luaV_finishOp, and require that any change to an opcode’s yield behavior in luaV_execute updates the table. The structure is simple enough that a flat switch is probably the right shape; the risk is drift between the two functions, not the branching.
luaK_finish — lcode.c
luaK_finish is a post-pass over the generated instructions that rewrites some of them: return variants, tail calls, vararg opcodes, and jump-to-jump optimization. Its cyclomatic complexity is 18, nesting is 3 and fan-out is 10, the most moderate profile of the five, but it earns its place because it has 11 touches in the last year and was last changed 3 days ago.
The excerpt shows another fall-through, from the return-variant case into the general return case, and several branches that depend on function-level flags (needing to close variables, using hidden arguments, having a vararg table). That means its behavior depends on state set elsewhere in the compiler, so a change in how those flags are set can alter what this pass does without touching this function.
My recommendation is the same shape as for luaK_posfix: replace the fall-through with an explicit helper for the return-rewriting logic, and consider one small function per rewrite rule. At complexity 18, this is the cheapest of the five to improve and a reasonable first refactoring win in lcode.c.
What I would do with this list
The five functions above are complex and being changed now, which is what separates them from critical code that is not moving.
If I had one afternoon, I would spend it on luaV_equalobj and luaK_posfix, where complexity is highest and both can be decomposed without touching the performance-sensitive dispatch loop.
Reproduce This Analysis
git clone https://github.com/lua/lua
cd lua
git checkout 8cb93bada561977eb370819e39d7d644e5fd0a3f
hotspots analyze . --mode snapshot --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →