Part of Language AI Handbook
Language models can explain code, detect bugs, support review, and run semantic code search. Covers dual encoders, contrastive training, and evaluation.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Code Understanding
Every large software project contains functions written months ago whose original authors have moved on, bug reports tied to stack traces that span dozens of files, and pull requests where reviewers must quickly assess whether a subtle refactor introduces regressions. For decades, these tasks fell entirely to human expertise. The emergence of code-aware language models has changed that equation: models can now explain what code does, flag potential defects, review patches for style and correctness, and retrieve relevant implementations from large repositories.
Code understanding covers everything a language model does with source code as input when the goal is analysis rather than generation. The distinction matters. Generating code requires the model to produce something new; understanding code requires the model to produce a faithful interpretation of something that already exists. This is a harder test in some ways, because the model's output can be verified against ground truth: a bug is real or it is not, an explanation matches the code or it does not.
The four capabilities covered in this chapter are grounded in a shared technical substrate but address different phases of the software development lifecycle. Explanation supports onboarding and maintenance: when a developer joins a project or returns to code they have not touched in months, understanding what existing code does is the first bottleneck. Bug detection supports quality assurance: catching defects before they reach production is cheaper than fixing them after, and a model that flags likely errors before a commit saves engineering time at scale. Code review supports collaboration: it automates the first pass of the review process, freeing human reviewers to focus on the higher-order concerns that require judgment. Code search supports discovery: finding the right function in a large codebase is a prerequisite for using it, and semantic search makes that discovery practical without requiring the developer to know the exact vocabulary used by the original author.
This chapter covers four core capabilities: code explanation, bug detection, code review, and code search. These are not independent; all four rest on the same foundation of code representations and semantic understanding. We will examine how models build that understanding, how each capability works mechanically, how they are implemented in practice, and where each one currently falls short.
Code understanding refers to a language model's ability to analyze existing source code and produce a faithful interpretation: explaining behavior, identifying defects, reviewing quality, or retrieving semantically relevant implementations.
How Models Represent Code
Code is text, and modern language models process it as such. But code has structural properties that reward special handling. Understanding what makes code different from natural language, and how models exploit or ignore those differences, is the foundation for understanding every capability covered in this chapter.
Code as a Sequence of Tokens
The simplest approach is to treat source code as a stream of tokens using the same byte-pair encoding (BPE) tokenizers used for natural language. This works better than it might seem. Natural language text and code share a surprising amount: variable names are English words or abbreviations, docstrings are prose, comments explain intent, and keywords form a fixed vocabulary that appears repeatedly.
The key difference is that code has formal semantics. The expression x = x + 1 has a precise meaning defined by the language specification, regardless of what the programmer intended. Natural language is much more tolerant of ambiguity. A model that has seen enough code learns to respect formal semantics, but this has to emerge from training rather than being engineered in.
Large models like Code Llama, StarCoder, and GPT-4 achieve strong code understanding using standard transformer architectures with sequence-level tokenization. The gains over earlier approaches come almost entirely from scale and data quality. This is an important empirical finding: you do not need a fundamentally different architecture to handle code; you need enough code in training and enough model capacity to internalize it.
One subtle issue with sequence tokenization is that BPE tokenizers trained primarily on natural language can produce poor tokenizations for code. Identifiers like calculateRunningAverage may be split at unexpected boundaries, and code-specific patterns like indentation-as-structure in Python can consume a disproportionate share of the token budget. Models like StarCoder explicitly train their tokenizers on code-heavy data to mitigate this. At the same time, the problem is often less severe than expected: even suboptimal tokenization is compensated by the model learning to reconstruct structure from token sequences during pretraining.
Structure-Aware Representations
An alternative is to incorporate code structure explicitly. Source code has a well-defined parse tree: an abstract syntax tree (AST) that represents the grammatical structure of the program. Some architectures process the AST rather than or in addition to the token sequence.
An abstract syntax tree is a tree representation of the syntactic structure of source code. Each node represents a construct in the language (function definition, binary operation, variable reference). The AST captures the nesting and composition of code elements without surface formatting details like whitespace or parentheses.
Structure-aware approaches offer the potential to capture relationships that are difficult to express as flat sequences: the control flow from an if condition to its two branches, the data flow from a variable assignment to its uses, the call graph linking function calls to their definitions. Graph neural networks have been applied to code graphs that combine ASTs with control-flow and data-flow edges, producing representations that are theoretically richer than sequence-only approaches.
Research from Microsoft and Facebook explored models like GGNN (Gated Graph Neural Networks) on program analysis tasks. The idea was to augment the AST with additional edge types representing semantic relationships. A "data flow" edge connects a variable assignment to every subsequent use of that variable. A "control flow" edge connects every statement to the next one that could execute after it. Together, these edges form a rich relational graph that a GNN can reason over, traversing paths that pure sequence models cannot directly represent.
The tradeoff is complexity and scale. Building an AST requires that the code parses successfully, which fails for incomplete snippets, code with syntax errors, and many scripting languages with dynamic constructs. Constructing data-flow graphs requires static analysis, which is computationally expensive for large codebases and becomes imprecise in the presence of dynamic dispatch or reflection. Training GNNs at the scale needed to match transformer performance on code has also proven difficult, because graph batching, gradient flow through graph structures, and efficient GPU utilization are all harder for graphs than for sequences.
In practice, the performance gap between structure-aware and sequence-only models has narrowed substantially as model scale has increased. Transformers trained on enough code implicitly learn structural relationships from co-occurrence patterns. A function call and the function definition it corresponds to appear close together in training in enough contexts that the model learns to associate them without explicitly constructing a call graph. This mirrors how language models learn syntactic dependencies from surface form.
Structure-aware representations remain useful for certain tasks where the structure is the point: verifying correctness of recursive functions, tracing data flow for security analysis, or checking type consistency. But for the broad code understanding tasks covered in this chapter, sequence-level models with sufficient scale are competitive with and often superior to purpose-built structure-aware approaches.
Semantic Understanding via Pretraining
The most important source of code understanding is pretraining on large code corpora. Models like CodeBERT, UniXcoder, and Code Llama are trained on hundreds of billions of tokens of source code across many programming languages. This training teaches the model several distinct but interrelated things:
- Syntax: which token sequences are valid in each language, so the model assigns low probability to syntactically illegal continuations
- Semantics: what operations do and what they produce, so the model can reason about side effects and return values
- Idioms: common patterns for accomplishing standard tasks, so the model recognizes
for i, v in enumerate(lst)as idiomatic Python - Cross-language transfer: how the same concept is expressed in Python, Java, JavaScript, and C++, allowing reasoning that generalizes across languages
- Code-comment alignment: how docstrings and inline comments relate to the code they describe, which is the foundation for both explanation and search tasks
Code-comment alignment deserves special emphasis. When a model trains on millions of examples where a function appears with a docstring, it learns to associate natural language descriptions with their implementations. This alignment is not explicitly supervised; it emerges from the training objective, which is to predict the next token in a sequence that interleaves code and natural language. The model learns the correspondence because predicting the docstring continuation requires understanding the code, and predicting the code continuation requires understanding the docstring.
This is why code explanation and code search both work as well as they do: the pretraining objective has already forced the model to learn the very mapping that these tasks require. Fine-tuning on task-specific data then sharpens this mapping for a particular output format or retrieval criterion.
The Role of Program Semantics
There is a deeper question about whether current language models truly understand code or merely correlate its surface patterns. A model can learn that functions named sort tend to return sorted outputs without understanding the sorting invariant. It can learn that for i in range(len(arr)) iterates over indices without understanding that this is equivalent to iterating over values when the loop body only reads values.
This distinction matters for correctness. A model that has learned surface correlations will produce confident-sounding explanations that are correct on average but wrong in specific cases that deviate from the training distribution. A model that has internalized program semantics would generalize correctly to novel programs.
The current evidence suggests that large language models occupy a middle ground. They have internalized a substantial amount of program semantics, particularly for common languages and idioms. They can correctly reason about the behavior of functions on typical inputs, trace variable values through simple sequences of operations, and identify violations of common conventions. But they fail on programs that require extended chains of logical reasoning, programs that exploit subtle language semantics, and programs where the bug is a single-character deviation from correct code that is otherwise syntactically normal.
This is not a failure of architecture but a reflection of training data: the model has seen millions of sorting functions but relatively few programs that expose off-by-one errors in non-trivial sorting invariants. Scale continues to push the boundary of what models can handle.
Code Explanation
The most accessible code understanding task is explanation: given a function or snippet, produce a natural language description of what it does.
What Explanation Requires
Useful explanation goes beyond surface translation. Turning for i in range(len(arr)) into "iterates over the indices of the array" is a start, but it tells the reader nothing they could not read themselves. Good explanation addresses four distinct concerns:
- What the code does: the observable behavior, inputs, outputs, and side effects
- Why it does it that way: the design choices, their rationale, and what alternatives exist
- When it is appropriate to use: the preconditions, post-conditions, and edge cases
- What it does not do: the cases it does not handle, the failure modes, and the assumptions it makes
The difference between restating code and explaining code is similar to the difference between reading a mathematical proof step-by-step versus understanding the idea behind it. The former is faithful but not illuminating; the latter requires semantic understanding. An explanation of a binary search function that just says "checks if the middle element matches the target and narrows the range" has captured the mechanics but missed the insight: binary search works because each comparison eliminates half the remaining candidates, giving logarithmic time complexity. The explanation becomes useful only when it communicates that insight.
This is why generating good explanations is hard for language models. The model must produce output that is simultaneously faithful to the code, informative beyond what the code itself says, and calibrated to the background knowledge of the reader. Getting all three right requires a rich model of both code semantics and the pragmatics of communication.
Explanation in Practice
Models generate explanations by treating code as the prompt and generating natural language as the completion. Fine-tuning on code-documentation pairs (such as those mined from GitHub repositories where functions appear alongside docstrings) substantially improves explanation quality over zero-shot prompting.
The CodeSearchNet dataset, introduced by Husain et al. (2019), assembled 6 million code-documentation pairs from GitHub across six programming languages. It became a standard training and evaluation resource for code-to-natural-language tasks. Models fine-tuned on CodeSearchNet can produce fluent summaries of short functions with reasonable accuracy.
Longer code units are harder. Length alone does not explain the difficulty: a class has methods, attributes, inheritance relationships, and invariants that interact. A good explanation of a class is not the union of explanations of its methods; it requires understanding how the parts compose into a coherent design. The explanation of a class like BinarySearchTree is not "a class with insert, delete, and search methods" but rather "a data structure that maintains a sorted binary tree for efficient search operations, with O(log n) average-case time complexity when the tree is balanced." This design-level description requires understanding how the parts compose, which is beyond the reach of models that process each method in isolation.
Instruction-tuned models prompted with explicit tasks, such as "Explain this function's purpose, time complexity, and edge case behavior in three sentences," produce more focused explanations than models prompted simply with the function text. The prompt structure guides the model toward the aspects of explanation that matter most.
Explanation Quality and Hallucination
One of the most dangerous failure modes in code explanation is confident hallucination: the model produces an explanation that sounds accurate but describes behavior the code does not have. This is not random noise; the model is filling in what the function "should" do based on its name, structure, and patterns from training. For common patterns, this works. For less common ones, the model may silently substitute its prior expectation for careful analysis.
Consider a function named sanitize_input that, due to a missing branch, fails to sanitize one particular injection pattern. A model that has seen thousands of correct sanitization functions may explain this function as "validates and cleans user input to prevent injection attacks," which is exactly wrong in the case that matters. The developer who reads this explanation and trusts it has been misled in a security-critical context.
Grounding explanations in symbolic analysis can reduce this risk. If the explanation is generated alongside dynamic analysis results (what the function returns for a set of test inputs), the model can be required to produce explanations consistent with observed behavior rather than expected behavior. This approach is more expensive but substantially more reliable for high-stakes code.
Worked Example: Explaining a Utility Function
Consider this Python function:
def find_duplicates(lst):
seen = set()
duplicates = set()
for item in lst:
if item in seen:
duplicates.add(item)
else:
seen.add(item)
return sorted(duplicates)A surface translation might be: "Uses two sets, seen and duplicates, iterates over the list, adds items to seen or duplicates depending on whether they were seen before, and returns the sorted duplicates set."
A semantic explanation would be: "Returns the sorted list of elements that appear more than once in the input. The function uses a hash set for O(1) membership testing, which gives overall O(n) time complexity. It finds duplicates in a single pass without modifying the original list. Note that it only detects elements appearing at least twice; it does not count how many times each appears."
The second explanation requires understanding the algorithmic intent, the data structures, the complexity, and the behavior on edge cases like empty lists or lists with no duplicates. This level of explanation helps a reader encountering unfamiliar code. Notice also that the second explanation identifies what the function does not do: it does not count occurrences, so an element appearing three times is indistinguishable from one appearing twice. This negative information is just as important as the positive description.
Code Implementation: Generating Explanations
Let's implement code explanation using the Hugging Face transformers library with a code-aware model.
We start by loading a small instruction-tuned model capable of code understanding. We will use Salesforce/codegen-350M-mono for lightweight demonstration, but larger models like codellama/CodeLlama-7b-Instruct-hf produce substantially better results in practice.
import warnings
warnings.filterwarnings("ignore")
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Salesforce/codegen-350M-mono"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model_explain = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.float32
)
model_explain.eval()def explain_code(code_snippet: str, max_new_tokens: int = 120) -> str:
"""Generate a natural language explanation for a code snippet."""
prompt = (
f"# Python function\n{code_snippet}\n# What this function does:\n# "
)
inputs = tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
outputs = model_explain.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.2,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
explanation_start = generated.find("# What this function does:\n# ") + len(
"# What this function does:\n# "
)
explanation = generated[explanation_start:].strip().split("\n")[0]
return explanationsnippets = {
"find_duplicates": """\
def find_duplicates(lst):
seen = set()
duplicates = set()
for item in lst:
if item in seen:
duplicates.add(item)
else:
seen.add(item)
return sorted(duplicates)""",
"binary_search": """\
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1""",
}
explanations = {name: explain_code(code) for name, code in snippets.items()}Function: find_duplicates Explanation: # 1. Find all the duplicates in the list. Function: binary_search Explanation: # - arr[mid] == target
The model generates completions that continue the comment begun in the prompt. For well-known algorithmic patterns like binary search, even a 350M-parameter model often produces accurate explanations. For more specialized code, larger models with instruction-following fine-tuning produce noticeably better results.
Bug Detection
Bug detection asks the model to identify defects in code: logic errors, null pointer dereferences, off-by-one errors, resource leaks, security vulnerabilities, and other issues that cause programs to behave incorrectly.
Why Automated Bug Detection Matters
The cost of finding a bug grows with how late in the development process it is discovered. A bug caught by a developer's own review before committing costs almost nothing. The same bug caught during code review is slightly more expensive. The same bug caught in testing costs significantly more. The same bug deployed to production and causing an incident can cost orders of magnitude more in engineering time, data loss, and reputation. This economics argument has driven investment in static analysis tools since the 1970s.
Traditional static analysis tools operate on formal rules. They parse the code, extract a control-flow graph or data-flow graph, and check that certain properties hold: every allocated resource is freed, every null pointer is checked before dereferencing, no format string vulnerability exists. This approach is precise and explainable, but it is limited to the properties someone has written rules for. Language models offer a different approach: learn from the empirical distribution of defects in real codebases, which captures a much wider range of bug patterns than any fixed rule set.
The distinction is important. Rule-based analyzers have high precision on the categories they handle but zero recall on categories they do not. Language model-based detectors have lower precision on the categories they handle but can generalize across a broader distribution of bug types, including novel ones that no rule has been written for.
Types of Bugs That Models Detect Well
Not all bugs are equally amenable to language model detection. Models perform best on bugs that have strong surface patterns in their training data:
- Common logic errors: Incorrect operator (
<=versus<in loop bounds), wrong variable name, missing return statement. - API misuse: Calling a library function with arguments in the wrong order, forgetting to check a return value, using a function that does not exist in the installed version.
- Security vulnerabilities: SQL injection via string formatting, path traversal, hardcoded credentials. These patterns appear extensively in security-focused training sets and audit datasets.
- Type errors: Using a string where an integer is expected, treating None as a valid value without a guard.
- Off-by-one errors: The classic
range(len(arr))versusrange(len(arr) - 1)distinction in array traversal.
Models trained on curated defect datasets, such as CodeBERT fine-tuned on the Defects4J benchmark, achieve substantially higher detection rates on these categories than zero-shot prompting.
What Makes Bug Detection Hard
The fundamental difficulty is that bug detection is an undecidable problem in general. Rice's theorem establishes that any non-trivial semantic property of programs is undecidable: there is no algorithm that correctly classifies all programs as having or not having a given property. Language models do not escape this; they make probabilistic predictions that are accurate on distributions similar to their training data but unreliable in novel contexts.
Several specific challenges compound this:
Context dependence. Whether a piece of code is correct depends on its context. A function that returns None on failure is correct if its callers check for None; it is a bug if callers blindly use the return value. A model that sees only the function cannot determine which case applies. This is why repository-level context, which we will discuss in a later chapter, is so important for bug detection: understanding a function requires understanding how it is called.
Specification dependence. A bug is a violation of a specification, but specifications are rarely written down. The model must infer what the code is supposed to do from the code itself, which is circular: the code might faithfully implement an incorrect specification. If a developer implemented a sorting function that returns elements in descending order by mistake, the code is internally consistent and there is no surface signal that distinguishes it from intentional descending sort.
False positives. Models tend to over-report issues, flagging unconventional but correct code as potentially buggy. This reduces the usefulness of bug detection in practice, because developers cannot afford to investigate every flagged location. Tools like Coverity found that even the most precise commercial static analyzers produced false positive rates of 10-30% on real code, and developer surveys consistently find that high false positive rates are the primary reason developers disable or ignore static analysis tools.
Adversarial patterns. Bugs deliberately hidden in code, as in security research on Trojan attacks, are much harder to detect than accidental mistakes. The surface form of malicious code may be identical to correct code except for a single crafted operand. Studies on code Trojans found that models trained on clean code had near-zero detection rates on deliberately planted backdoors.
Mathematical Formulation: Defect Prediction as Classification
Formally, bug detection can be cast as binary classification. Given a code snippet and a label where indicates a defect, the model learns a scoring function:
where are the model parameters. For a fine-tuned encoder like CodeBERT, this is implemented by adding a linear classification head on top of the [CLS] token representation:
where:
- : the
[CLS]token hidden state from the final encoder layer, which aggregates information about the entire snippet - : a learned weight vector for the classification head
- : a bias term
- : the sigmoid function, which maps the logit to a probability in
The model is fine-tuned by minimizing binary cross-entropy loss over labeled defect examples:
where is the number of training examples. At inference time, a threshold converts the probability to a binary prediction: report a defect if .
The threshold controls the precision-recall tradeoff. A lower threshold catches more real bugs but generates more false positives. A higher threshold reduces false positives at the cost of missing more defects. In practice, is tuned on a held-out validation set to match the operational requirements of the deployment context.
The Precision-Recall Tradeoff in Deployment
The precision-recall tradeoff is a technical concern with a practical consequence: it determines whether a bug detection tool gets adopted. Consider two deployment scenarios.
In a continuous integration pipeline, every commit is checked and a developer sees the results before merging. Here, precision matters most: a developer who sees ten flagged issues and investigates all of them to find none are real bugs will quickly disable the tool. A precision of 70-80%, meaning seven or eight real bugs out of every ten flagged, may be acceptable in this context.
In a batch security audit of a large codebase, the goal is to find as many real issues as possible, and a security engineer has time to investigate. Here, recall matters most: missing a real vulnerability has higher cost than spending time on false positives. The same model might be deployed with a lower threshold in this context.
This deployment-context sensitivity means that bug detection systems should not be evaluated by a single metric. The ROC curve, which plots true positive rate against false positive rate across all possible thresholds, gives a complete picture of the tradeoff. The area under the ROC curve (AUC) is a threshold-independent measure of overall discrimination ability.
Code Implementation: Bug Detection
Let's implement a simple bug detection system using CodeBERT.
from transformers import RobertaForSequenceClassification, RobertaTokenizer
bug_model_name = "microsoft/codebert-base"
bug_tokenizer = RobertaTokenizer.from_pretrained(bug_model_name)
bug_model = RobertaForSequenceClassification.from_pretrained(
bug_model_name, num_labels=2
)
bug_model.eval()def detect_bug_score(code_snippet: str) -> float:
"""Return a defect probability score for a code snippet."""
inputs = bug_tokenizer(
code_snippet,
return_tensors="pt",
max_length=512,
truncation=True,
padding=True,
)
with torch.no_grad():
logits = bug_model(**inputs).logits
probs = torch.softmax(logits, dim=-1)
return probs[0, 1].item()code_pairs = [
{
"name": "Off-by-one in loop",
"buggy": """\
def sum_array(arr):
total = 0
for i in range(len(arr) + 1):
total += arr[i]
return total""",
"fixed": """\
def sum_array(arr):
total = 0
for i in range(len(arr)):
total += arr[i]
return total""",
},
{
"name": "Missing null check",
"buggy": """\
def get_length(s):
return len(s)""",
"fixed": """\
def get_length(s):
if s is None:
return 0
return len(s)""",
},
]
scores = []
for pair in code_pairs:
buggy_score = detect_bug_score(pair["buggy"])
fixed_score = detect_bug_score(pair["fixed"])
scores.append(
{
"name": pair["name"],
"buggy_score": buggy_score,
"fixed_score": fixed_score,
}
)Scenario Buggy Score Fixed Score Correct? ----------------------------------------------------------------- Off-by-one in loop 0.6104 0.6094 yes Missing null check 0.5974 0.6025 no
The base CodeBERT model without defect-specific fine-tuning assigns similar scores to both variants. In practice, a fine-tuned checkpoint trained on the Devign or CodeXGLUE defect datasets assigns clearly higher scores to the buggy variants. The structure of the pipeline is the same; only the weights change.

The chart makes the lack of discrimination visible. An effective detector would show clearly taller buggy bars for every scenario, but these gaps are tiny and the missing-null-check pair is reversed. Defect-specific fine-tuning is what turns the pairwise score gap into a useful signal.
Code Review
Code review combines explanation and bug detection into a higher-level task: given a diff or a change set, produce a structured assessment of code quality, correctness, style, and maintainability.
What Code Review Covers
Professional code review addresses multiple concerns simultaneously. The distinction from bug detection is important: bug detection asks a binary question (is there a defect?) while code review asks a qualitative question (is this code good, and if not, what should be improved?). This makes code review more open-ended and harder to evaluate, because quality is not binary.
- Correctness: Does the code implement the intended behavior? Does it handle edge cases?
- Readability: Is the code understandable? Are names descriptive? Is the structure clear?
- Maintainability: Is the code organized in a way that will be easy to modify? Is complexity justified?
- Style: Does the code follow project conventions and language idioms?
- Security: Does the change introduce vulnerabilities?
- Performance: Does the change introduce unnecessary computational cost?
- Test coverage: Does the change include tests? Do the tests cover the important cases?
Human reviewers balance these concerns together and interact with the author in a dialogue that spans multiple rounds. Models currently approximate the first pass of this process: flagging specific issues and suggesting improvements, without the iterative dialogue.
The History of Automated Code Review
The idea of automated code quality feedback predates language models by decades. lint, the original C code checker released in 1978, analyzed source code for suspicious constructs that were legal but likely bugs. Tools like Checkstyle, PMD, and FindBugs followed in the Java ecosystem. These tools operate on hard-coded rules: "methods should not exceed 50 lines," "catch blocks should not be empty," "assignments inside conditionals are suspicious."
Rule-based tools scale well and have predictable behavior, but they address a fixed set of quality dimensions and cannot adapt to project-specific conventions. Language models offer a different capability: they have been trained on human-written code reviews, so they have learned what human reviewers consider important, including the context-specific judgments that are hard to encode as rules.
The CodeReviewer model from Microsoft (Li et al., 2022) was fine-tuned on 80,000 real code review comments from GitHub pull requests. In human evaluations, reviewers judged CodeReviewer-generated comments as useful in approximately 60% of cases, compared to 35% for a strong baseline. This is a meaningful improvement, though the 60% rate also shows how much room for improvement remains.
Review Quality: Actionable vs. Generic
The most important dimension of review quality is actionability. A comment that says "this function is complex" is not useful; a comment that says "this function has four nested conditional branches and could be simplified by extracting the inner branch into a helper function check_validity(x)" is useful.
Language models tend to produce generic comments when the prompt is underspecified. Training on human-written review comments from GitHub pull requests substantially improves specificity, because the model learns the style and precision of expert human reviewers. Research on the CodeReviewer model (Li et al., 2022) showed that fine-tuning on code review data produces reviews that human evaluators judge as significantly more useful than zero-shot prompted reviews.
The challenge is that review quality depends on context that is often absent from a single diff: project conventions, the purpose of the change, the history of the codebase, and the skill level of the author. A function that would be over-engineered in one context might be exactly right in another.
Worked Example: Reviewing a Python Change
Consider a pull request that adds a new utility function:
# Before (empty)
# After:
def process(items, threshold=0.5):
res = []
for i in items:
if i > threshold:
res.append(i * 2)
else:
res.append(i)
return resA surface-level review might note: "The function uses a loop that could be replaced with a list comprehension." That is correct but minor.
A deeper review would note:
- The function name
processand parameteritemsare too generic: what is being processed, and what are the items? - The docstring is missing: the caller cannot know what the function does without reading the implementation.
- The function does two different things to items above and below the threshold, and naming these paths explicitly would improve clarity.
- If the input contains negative values,
i * 2on a negative will produce a more negative value, which may or may not be intended. - Type annotations are absent. For a utility called from many places, type hints like
def process(items: list[float], threshold: float = 0.5) -> list[float]:would improve IDE support and error detection.
This review is more useful because it addresses both syntax and semantics, and it asks questions that help the author think through the design rather than just telling them to fix formatting. A language model producing this kind of review can accelerate the engineering process.
Code Implementation: Automated Code Review
Let's implement automated code review by prompting a code model with a structured review template.
def review_code(code: str, max_new_tokens: int = 200) -> str:
"""Generate a structured code review for the given code snippet."""
prompt = (
"# Code Review\n"
f"# Code to review:\n{code}\n"
"# Review (issues and suggestions):\n"
"# 1."
)
inputs = tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
outputs = model_explain.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.3,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
review_start = generated.find("# Review (issues and suggestions):\n") + len(
"# Review (issues and suggestions):\n"
)
review_text = generated[review_start:].strip()
lines = [l for l in review_text.split("\n") if l.strip().startswith("#")][
:5
]
return "\n".join(lines)code_to_review = """\
def process(items, threshold=0.5):
res = []
for i in items:
if i > threshold:
res.append(i * 2)
else:
res.append(i)
return res"""
review_output = review_code(code_to_review)Generated Code Review: # 1. The code should be able to handle the case when the number of items is less than the threshold # 2. The code should be able to handle the case when the number of items is greater than the threshold # 3. The code should be able to handle the case when the number of items is less than the threshold # 4. The code should be able to handle the case when the number of items is greater than the threshold # 5. The code should be able to handle the case when the number of items is less than the threshold
The quality of zero-shot review varies substantially with model size and instruction-following capability. For production review tooling, instruction-tuned models prompted with explicit criteria (naming, documentation, type hints, complexity) and fine-tuned on curated review datasets produce more consistent, actionable output.
Key Parameters for Review Quality
The parameters most important for code review quality are:
- temperature: Lower values (0.1-0.3) produce more consistent, less creative reviews. Higher values (0.7-1.0) can surface more diverse issues but at the cost of reliability.
- max_new_tokens: Review length. Too short cuts off multi-issue reviews; too long produces rambling output. 150-300 tokens works well for single-function review.
- prompt structure: Framing the review with explicit categories (correctness, style, documentation) guides the model to address all aspects rather than dwelling on the first issue it notices.
- model selection: Instruction-tuned checkpoints trained on human feedback produce substantially better reviews than base language models, even at smaller scale.
Diff-Level vs. Function-Level Review
There are two natural units for code review: the individual function or file (function-level) and the complete set of changes in a pull request (diff-level). Function-level review is simpler but misses review concerns that require understanding the interaction between changes across files. Diff-level review is richer but requires the model to handle longer contexts with deletions, additions, and unchanged context lines mixed together in unified diff format.
Models fine-tuned on diff-level data learn to reason across the change set. They can identify cases where a new function was added but the corresponding test was not, or where a change to a helper function is inconsistent with how callers in other files use it. These cross-file observations are much more valuable than per-function comments in isolation, but they require much more sophisticated context handling and are correspondingly harder to train for.
Code Search
Code search retrieves relevant code from a large repository given a natural language query. The goal is to find the function, class, or module that implements a given concept, without knowing the exact names or file locations.
The Semantic Gap
Traditional code search is lexical: the search engine finds code files that contain the query terms. This fails when the vocabulary of the query does not match the vocabulary of the code. A query for "how do I read a file line by line" will not match a function named iter_lines unless the function's docstring contains the phrase "line by line."
This vocabulary mismatch is called the semantic gap between natural language and code. It is a fundamental challenge because code authors choose names for reasons of brevity, convention, or domain familiarity, not to match the vocabulary that later users will use to search for the code. A function that implements exponential moving average might be named ema, smooth, running_average, or exp_weighted_mean, and a new user might search for any of these, or for "discount past values" or "weight recent observations more heavily."
Semantic code search addresses this gap by embedding both queries and code into a shared vector space where semantically similar items are close. A natural language query and the code that implements the described behavior should be near each other in this space, even if they share no words. This is a multimodal embedding problem: the encoder must handle both natural language and code as inputs and map them to a common space.
The connection to the pretraining insight from earlier in this chapter is direct: models pretrained on code-documentation pairs have already learned this alignment, because predicting docstrings from code (and code from docstrings) is structurally equivalent to the embedding alignment problem. Fine-tuning with a contrastive objective sharpens this implicit alignment into explicit similarity scores.
Dual-Encoder Architecture
The standard architecture for semantic code search is the dual encoder, also called a bi-encoder:
- An NL encoder maps a natural language query to a vector .
- A code encoder maps a code snippet to a vector .
- Retrieval scores code snippets by , typically cosine similarity.
The two encoders may share weights (a single model handles both modalities) or be separate models. CodeBERT and UniXcoder use a shared encoder: the same transformer handles both natural language and code inputs, with the modality distinction emerging from the training distribution. This parameter sharing is efficient and, more importantly, encourages the model to learn representations that are aligned across modalities rather than developing separate representation spaces that must then be bridged.
The two encoders are trained jointly to maximize similarity between matching query-code pairs and minimize similarity between non-matching pairs. The contrastive loss for a batch of query-code pairs is:
where:
- : cosine similarity between query and code snippet
- : a temperature parameter controlling how sharply the softmax discriminates between close and distant pairs
- The numerator corresponds to the correct pair ; the denominator sums over all code snippets in the batch as negatives
This is the InfoNCE loss, the same objective used in SimCLR for visual representation learning and in CLIP for image-text alignment. The temperature is a critical hyperparameter. When is large, the softmax is flat and the loss signal is weak, because even very dissimilar pairs contribute substantially to the denominator. When is small, the softmax is sharp and the model is penalized heavily for any small misranking. In practice, is learned or set based on a small grid search, with values around 0.07 commonly used for contrastive code search.
The model learns to push the correct query-code pair close together in embedding space while pushing all other code snippets in the batch away from the query. In-batch negatives, where all other code snippets in the same batch serve as negatives for each query, are computationally efficient: each forward pass provides negative examples for each of the queries in the batch.
At inference time, all code snippets in the repository are encoded once and indexed using an approximate nearest neighbor (ANN) index such as FAISS. A new query is encoded and then searched against the index in sub-linear time.
ANN algorithms find vectors close to a query vector without exhaustively comparing against all indexed vectors. FAISS (Facebook AI Similarity Search) and ScaNN are popular implementations. ANN makes semantic search over millions of code snippets practical by trading a small accuracy loss for orders-of-magnitude speedups.
Training Data
The quality of the code search model depends heavily on the quality and quantity of query-code pairs used for training. Several sources provide these pairs:
Docstring-code pairs: Function definitions paired with their docstrings are natural query-code pairs. CodeSearchNet assembled 6 million such pairs from GitHub.
Stack Overflow pairs: A natural language question paired with an accepted code answer. Questions on Stack Overflow are often exactly the kind of queries a developer would make to a code search engine.
Synthetic pairs: Large language models can generate queries for code snippets or generate code for queries, expanding the training set beyond what exists in public repositories.
Hard negatives: Training pairs where the negative code snippet is semantically close but not the correct answer. Hard negatives prevent the model from relying on superficial differences and force it to learn fine-grained semantic distinctions. For example, pairing a query "find the minimum element" with both a find_max function (hard negative) and an unrelated string operation (easy negative) forces the encoder to distinguish more precisely between minimum and maximum semantics. Easy negatives alone produce models that can find the right function in a diverse codebase but fail when many functions are semantically similar.
Re-Ranking and Hybrid Search
Dual-encoder retrieval is fast but imprecise at fine-grained ranking. The alternative is a cross-encoder, which processes the query and each candidate code snippet together and produces a scalar score. Cross-encoders can compute arbitrary interactions between query tokens and code tokens (because both appear in the same forward pass), making them much more accurate than dual encoders at fine-grained ranking. But they cannot be pre-computed: every query requires a forward pass against every candidate snippet, making them impractical for large codebases when used alone.
The standard solution is two-stage retrieval. The dual encoder retrieves the top-K candidates quickly (typically ). The cross-encoder then re-ranks those candidates, selecting the top few results to return to the developer. This hybrid approach gets the recall benefits of fast approximate retrieval with the precision benefits of expensive joint scoring. Products like GitHub Code Search use architectures of this general form.
Code Implementation: Semantic Code Search
Let's implement semantic code search using the sentence-transformers library with a code-aware embedding model.
from sentence_transformers import SentenceTransformer
search_model = SentenceTransformer("all-MiniLM-L6-v2")code_repository = {
"binary_search": """\
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1""",
"merge_sort": """\
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
return result + left[i:] + right[j:]""",
"find_duplicates": """\
def find_duplicates(lst):
seen = set()
duplicates = set()
for item in lst:
if item in seen:
duplicates.add(item)
else:
seen.add(item)
return sorted(duplicates)""",
"word_count": """\
def word_count(text):
words = text.lower().split()
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
return counts""",
"flatten_list": """\
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result""",
}code_names = list(code_repository.keys())
code_snippets = list(code_repository.values())
code_embeddings = search_model.encode(code_snippets, normalize_embeddings=True)
def semantic_search(query: str, top_k: int = 3) -> list:
"""Retrieve top_k most relevant code snippets for a natural language query."""
query_embedding = search_model.encode([query], normalize_embeddings=True)[0]
scores = code_embeddings @ query_embedding
ranked_indices = np.argsort(scores)[::-1][:top_k]
results = []
for idx in ranked_indices:
results.append(
{
"name": code_names[idx],
"score": float(scores[idx]),
}
)
return resultstest_queries = [
"find an element in a sorted array",
"count word frequency in text",
"remove duplicates from a list",
"sort a list recursively",
"recursively flatten a nested list structure",
]
search_results = {q: semantic_search(q, top_k=2) for q in test_queries}Query: 'find an element in a sorted array' Rank 1: find_duplicates (score=0.3704) Rank 2: binary_search (score=0.3511) Query: 'count word frequency in text' Rank 1: word_count (score=0.7495) Rank 2: find_duplicates (score=0.1245) Query: 'remove duplicates from a list' Rank 1: find_duplicates (score=0.6154) Rank 2: merge_sort (score=0.2775) Query: 'sort a list recursively' Rank 1: merge_sort (score=0.4107) Rank 2: find_duplicates (score=0.3764) Query: 'recursively flatten a nested list structure' Rank 1: flatten_list (score=0.6671) Rank 2: merge_sort (score=0.2401)
The similarity scores reflect semantic closeness, but not perfectly. The word-frequency query retrieves word_count, while the sorted-array query ranks find_duplicates just above binary_search. That miss is a useful reminder that this general sentence encoder was not trained specifically for code retrieval. A code-specific embedding model such as UniXcoder or CodeBERT fine-tuned on code search generally improves the ranking.

The heatmap reveals both the intended structure and the model's mistakes. Four rows peak on the expected function, while the first row's off-diagonal maximum shows the sorted-array retrieval error. Other off-diagonal cells capture semantic overlap: the model associates both merge_sort and find_duplicates with list operations even when only one is the intended target.
Evaluation Metrics for Code Search
Code search systems are evaluated using metrics adapted from information retrieval. The two most common are Mean Reciprocal Rank and Recall at K.
MRR (Mean Reciprocal Rank): For each query, the reciprocal of the rank of the first correct result, averaged over queries. A model that consistently puts the correct answer first achieves MRR = 1.0. If the correct answer appears at rank 2, the reciprocal rank is 0.5. If it appears at rank 5, the reciprocal rank is 0.2.
where is the rank position of the correct code snippet for query .
R@K (Recall at K): The fraction of queries for which the correct answer appears in the top results. R@1 is the strictest, requiring the correct answer to be the top result. R@10 is more lenient but reflects realistic usage: developers will often scan the top 10 results to find what they need.
On the CodeSearchNet benchmark, current models achieve MRR around 0.70-0.85 depending on programming language, with Python and JavaScript showing stronger performance than Go and Ruby due to training data volume. This reflects an important practical consideration: the quality of code search degrades for languages with less public code, independent of the model architecture.
Practical Deployment Considerations
Deploying semantic code search in a real engineering organization introduces several challenges beyond model quality.
Index freshness. Code repositories change continuously. The ANN index must be updated as functions are added, modified, and deleted. Incremental index updates are more efficient than full rebuilds but require careful management to avoid stale results.
Granularity. Functions are the natural unit for code search, but they are not always the right granularity. A developer searching for "how does the authentication system work" may want a higher-level answer that spans many functions. A developer searching for "the specific line that writes to the audit log" wants sub-function granularity. Systems that index at multiple granularities and blend results across levels handle a wider range of queries.
Private code and confidentiality. Enterprise code search requires that the model not leak proprietary code through its embeddings. This is less of a concern for retrieval systems, where the model only produces vectors, than for generation systems that might reproduce training code verbatim. Nonetheless, access control is important: a search query from one team should not return results from another team's private repository.
Limitations and Impact
Code understanding has made real progress, but the gap between current capabilities and what engineers need is still large in several important ways.
Hallucination in explanation. Language models can produce explanations that sound confident and accurate but describe behavior the program does not have. This is particularly dangerous for security-critical code: a model that confidently states a function "validates all inputs before processing" when it does not may be worse than no explanation, because the developer trusts the explanation and skips manual verification. Grounding explanations in symbolic analysis, such as running the code and comparing against the explanation's predictions, can reduce this risk.
Bug detection precision. Real-world defect detection tools face a difficult precision-recall tradeoff. Developers using GitHub Copilot and similar tools report that AI-generated bug reports have a high false positive rate: many flagged locations are not bugs. When the false positive rate is too high, developers ignore all reports, defeating the purpose. Improving precision without sacrificing recall is an open research problem.
Review depth and context. Automated code review produces comments that address surface-level issues but rarely catches design-level problems: wrong abstractions, missing invariants, violation of domain-specific conventions. These deeper issues require understanding the intent of the change and the context of the larger system, which is outside the reach of models that see only a single diff. Systems that maintain a persistent representation of the codebase across reviews, updating as the codebase evolves, would be substantially more useful but are significantly harder to build.
Code search at enterprise scale. Semantic search works well on small repositories with clean docstrings. At enterprise scale, codebases contain millions of functions, many with no documentation, inconsistent naming, and redundant implementations scattered across legacy code. Approximate nearest neighbor search introduces recall errors; index maintenance as the codebase changes is an operational challenge; and the embedding quality degrades for domain-specific languages and frameworks with limited training data. These are engineering problems as much as research problems.
Evaluation difficulty. One underappreciated challenge in code understanding is evaluation. For code search, we have established benchmarks like CodeSearchNet with clear metrics. For code explanation and code review, evaluation is much harder: there is no single correct explanation of a function, and no single correct review of a change. Human evaluation is expensive and has its own consistency challenges. Automated metrics like BLEU correlate weakly with human judgments of quality. This makes it difficult to measure progress reliably, which in turn makes it difficult to direct research effort toward the capabilities that matter most in practice.
Despite these limitations, the impact of code understanding is already substantial. Static analysis tools augmented with language models catch categories of bugs that rule-based analyzers miss. Semantic code search in products like GitHub Code Search has made it practical to search unfamiliar codebases without knowing the exact naming conventions. Automated explanation has lowered the barrier to understanding open-source code for developers new to a project. And code review assistance, while imperfect, already accelerates reviewer throughput on routine changes, freeing reviewers to focus their attention on the design-level concerns that require human judgment.
The underlying enabler for all of these capabilities is the same: large-scale pretraining on code taught models a rich model of code semantics that generalizes to understanding tasks without task-specific training. This mirrors the lesson from natural language understanding: scale and data quality produce capabilities that are very difficult to engineer by hand. The boundary of what language models can understand about code has been pushed outward with each generation of models, and there is no obvious reason to expect that boundary to stop moving.
Summary
Code understanding encompasses four closely related capabilities, each building on a common foundation of code representations learned from large code corpora:
-
Code explanation converts code into natural language descriptions. The challenge is producing explanations that capture algorithmic intent, complexity, and edge cases, not just restating the syntax. Dual-objective training on code-docstring pairs enables models to bridge the natural language-code gap. The key risk is hallucination, where the model confidently describes behavior the code does not have.
-
Bug detection classifies code snippets as defective or correct. Framed as binary classification with a fine-tuned encoder, it achieves useful accuracy on common bug patterns. The fundamental difficulty is that detecting all bugs is undecidable, and real-world deployment requires carefully balancing precision against recall to avoid the false-positive fatigue that causes developers to ignore detection tools altogether.
-
Code review combines explanation and defect detection into structured quality assessments. Fine-tuning on human-written review data substantially improves actionability. Current systems handle surface-level issues well but struggle with design-level feedback that requires understanding intent and system context. Diff-level review, which reasons across multiple changed files, is more powerful than function-level review but requires longer context handling.
-
Code search retrieves semantically relevant code given a natural language query. The dual-encoder architecture with contrastive training maps queries and code into a shared embedding space. Two-stage retrieval, combining fast approximate ANN search with precise cross-encoder re-ranking, achieves both speed and accuracy. Evaluation uses MRR and R@K from information retrieval. Code-specific embeddings like UniXcoder and CodeBERT significantly outperform general sentence encoders on code retrieval benchmarks.
The next chapter on code completion, Part XLVII Chapter 3, examines how models extend these understanding capabilities to generate new code in context, combining retrieval with generation for fill-in-the-middle and repository-level completion tasks.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about code understanding with language models.
Code Understanding Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!