Lua's VM core carries the highest activity risk — 5 functions to address first

Three of the five highest-risk functions in lua/lua sit in lvm.c, with the other two in lcode.c, and every one of them was changed within the last three days.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk2.25Low
Hottest FunctionluaV_equalobj

Run this on your own codebase

See if your own repo has a luaV_equalobj-style hotspot — run this in any local git repo:

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What makes fan-out of 120 in luaV_execute a problem in lua?

Fan-out is the count of distinct functions a function calls directly, and a value of 15 or more is already a strong coupling signal. luaV_execute calls 120 distinct functions, so a change to almost any runtime subsystem can affect it, and testing it in isolation is not realistic. Its branching is modest (cyclomatic complexity 11), so the difficulty is breadth of dependencies, not tangled control flow. Because it was changed 3 days ago and has 10 touches in the last year, any edit here deserves a full test run across opcodes.

How do I reduce cyclomatic complexity in c?

The usual technique is extract-method combined with decompose-conditional: pull each large switch case or nested branch into a named helper, so the original function only dispatches. As a rule of thumb, a cyclomatic complexity above 15 warrants splitting and above 30 warrants prompt attention; luaV_equalobj is at 29 and luaV_finishOp at 30. A concrete first step for luaV_equalobj is separating the different-variant comparison switch from the same-variant switch into two helpers, which should roughly halve the main function's path count. In C, I would also check for macros that hide branches, since they add paths the metric does not count.

Is lua actively maintained?

The data says yes. All five top functions are in the active tier, with 10 or 11 touches in the last year and only 3 days since last change. Across the repository, 890 of 1,296 functions are active and 406 are quiet. Active development and high structural complexity coexist here, which is why the same functions show up as both frequently changed and high-risk.

How do I reproduce this analysis?

Install the hotspots CLI from its GitHub repository, then check out the analyzed commit with `git checkout 8cb93ba`. Run `hotspots analyze . --mode snapshot --force` from the repository root. The same command works on any local git repository without configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines structural complexity (cyclomatic complexity, nesting depth and fan-out) with how much and how recently the file has changed, so functions that are both hard to understand and in changing code score highest. A very complex function that has not been touched in two years scores lower than a moderately complex one touched every week, because the dormant one has less near-term chance of a new bug being introduced. That ordering points review effort at code where a mistake is most likely to be made right now, not simply at code that looks complicated in the abstract.

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

FunctionFileRiskCCNDFO
luaV_equalobjlvm.c2.229516
luaV_executelvm.c2.2114120
luaK_posfixlcode.c2.129319
luaV_finishOplvm.c2.13029
luaK_finishlcode.c2.118310

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.

Activity Tier Distribution
Active890Quiet406

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

luaV_equalobj
lvm.c
2.25
critical
CC 29
ND 5
FO 16
touches/yr 10

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.

Cyclomatic Complexity 29
threshold: 10

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
lvm.c
2.2
critical
CC 11
ND 4
FO 120
touches/yr 10

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.

Fan-out 120
threshold: 15

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
lcode.c
2.12
critical
CC 29
ND 3
FO 19
touches/yr 11

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
lvm.c
2.11
critical
CC 30
ND 2
FO 9
touches/yr 10

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
lcode.c
2.07
critical
CC 18
ND 3
FO 10
touches/yr 11

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 →

Was this useful? Tell me more →

Related Analyses