Code Understanding › How Models Represent Code

Michael BrenndoerferFebruary 2, 20267 min read
Language AI HandbookAll chapters
Part 47 of 67Code Generation
Chapter 358Code Understanding › How Models Represent Code
Reading section 2 of 13 · How Models Represent Code · 7 minSection 2 of 13 · 7 min

Book contents

Section 2 of 13 · 7 min read

Code Understanding › How Models Represent CodeOne section · Sections in this chapter2 / 13 ↓
Reading tools Definitions & inline links

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 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.

Abstract Syntax Tree (AST)

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.