Summary
This incident examines a common misconception about inline functions in lexically scoped languages like Kotlin. The failure arises when developers assume that “inlining” means “copy‑paste the function body into the caller,” overlooking the strict rules of lexical scope resolution that still apply even after inlining.
Root Cause
The root cause is a misunderstanding of how lexical scope interacts with compile‑time inlining.
Inlining does not change where a function is allowed to resolve names—it only changes how the compiler emits code.
Key points:
- Lexical scope is determined before inlining occurs
- Name resolution happens in the function’s original scope, not the caller’s
- Inlining is a mechanical substitution, not a scope transplant
Why This Happens in Real Systems
Real compilers enforce lexical scoping rules long before optimization passes like inlining occur.
This leads to:
- The inline function being analyzed in its own scope first
- Any unresolved identifiers being treated as errors before inlining is even considered
- The compiler refusing to inline code that is not valid in its original context
In short: Inlining cannot rescue invalid code.
Real-World Impact
This misunderstanding can cause:
- Confusion about why inline functions cannot “see” caller variables
- Incorrect assumptions about how macros, lambdas, and inline functions behave
- Bugs when developers expect inline functions to behave like textual macros
Example or Code (if necessary and relevant)
Below is a minimal Kotlin example showing the failure:
inline fun someFunction() {
print(text) // ERROR: text is not in this scope
}
fun main() {
val text = "hi"
someFunction()
}
Even though the compiler could theoretically substitute the body, it refuses because the inline function is invalid in its own lexical scope.
How Senior Engineers Fix It
Experienced engineers recognize that inline functions cannot “capture” caller variables unless those variables are explicitly passed or captured via lambdas.
Typical fixes:
- Pass the variable as a parameter
- Use inline lambdas, which can capture caller scope
- Avoid relying on inlining for scope manipulation
Example fix:
inline fun someFunction(text: String) {
print(text)
}
Or using a lambda:
inline fun someFunction(block: () -> Unit) {
block()
}
fun main() {
val text = "hi"
someFunction { print(text) } // valid capture
}
Why Juniors Miss It
Junior engineers often:
- Conflate inlining with macro expansion
- Assume the compiler performs substitution before scope checking
- Misunderstand the difference between lexical scope and runtime behavior
- Expect inline functions to behave like C-style macros rather than scoped functions
The key takeaway: Inlining is an optimization, not a scoping mechanism.