WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
CS Fundamentals

Compilers and Interpreters: How Code Actually Runs

When you run a Python script or compile a Rust binary, a complex pipeline transforms human-readable source code into something a CPU can execute. Understanding the stages of that pipeline — lexing, parsing, semantic analysis, code generation — explains performance characteristics, error message formats, and the difference between compile-time and runtime errors.

Published June 30, 2026

The textbook distinction is: a compiler translates the entire source program to target code before any of it runs; an interpreter translates and executes the program incrementally. In practice, most production runtimes blend the two, and understanding where each language sits on the spectrum requires looking at the full pipeline.

Stage 1: Lexing (tokenization)

The first stage reads raw source text and produces a flat sequence of tokens — the smallest meaningful units of the language. Each token has a type (keyword, identifier, number literal, operator, punctuation) and a value.

Source code:
    x = 42 + y

Token stream:
    IDENTIFIER  "x"
    OP          "="
    NUMBER      42
    OP          "+"
    IDENTIFIER  "y"
    EOF

The lexer also strips whitespace and comments — these are meaningful to humans but irrelevant to the parser. Lexer errors are the messages you see for invalid characters: a byte that is not part of any valid token causes a lex error before any parsing begins.

Stage 2: Parsing

The parser takes the flat token stream and produces a tree that reflects the grammatical structure of the program: an Abstract Syntax Tree (AST). The AST encodes operator precedence and grouping without relying on the original text.

Token stream for: 2 + 3 * 4

AST:
    BinaryOp(+)
    left:  NumberLiteral(2)
    right: BinaryOp(*)
            left:  NumberLiteral(3)
            right: NumberLiteral(4)

The tree correctly represents that * binds more tightly than +.
Evaluation of this tree yields 14, not 20.

Syntax errors are caught at this stage. If you write if x = in Python, the parser sees an incomplete grammar rule and produces a SyntaxError with a line number. The error exists in the structure of the token sequence, not in the meaning of the program.

Stage 3: Semantic analysis

The AST is structurally valid, but the program may still be meaningless. Semantic analysis checks things the grammar cannot express: that variables are declared before use, that function calls have the right number of arguments, that types are compatible (in statically typed languages).

// C: semantic analysis catches a type error the parser cannot see
int add(int a, int b) { return a + b; }

add(1, "hello");   // parser accepts this: it is grammatically valid
                   // semantic analysis rejects it: wrong argument type

In statically typed languages, semantic analysis is where type checking happens. In dynamically typed languages, much of what would be a compile-time semantic error becomes a runtime error instead: the program is syntactically and grammatically valid, and the type mismatch only appears when that line executes.

Code generation: compilers

After analysis, a compiler translates the AST into target code. The target may be machine code for a specific CPU architecture (as in C, Rust, and Go), bytecode for a virtual machine (as in Java and Python), or another high-level language (as in TypeScript compiling to JavaScript).

Simplified view of a compiled C function:

Source:
    int square(int n) { return n * n; }

x86-64 assembly output (from gcc -O2):
    square:
        imul  edi, edi    ; n * n (result in edi)
        mov   eax, edi    ; move result to return register
        ret               ; return to caller

An optimizing compiler analyses the AST and intermediate representations to produce more efficient target code: eliminating redundant computations, reordering instructions to use CPU pipelines better, and inlining small functions to eliminate call overhead. This analysis takes time at build, which is why large Rust projects can be slow to compile.

Bytecode and virtual machines

Languages like Python and Java compile to an intermediate bytecode rather than native machine code. A virtual machine (VM) then interprets or executes that bytecode at runtime. Bytecode is more compact and portable than machine code; the same bytecode file runs on any machine that has the right VM.

# Inspect Python bytecode with the dis module
import dis

def add(a, b):
    return a + b

dis.dis(add)
# Output:
#   LOAD_FAST   0 (a)
#   LOAD_FAST   1 (b)
#   BINARY_OP   0 (+)
#   RETURN_VALUE

CPython (the standard Python interpreter) compiles to bytecode and then interprets that bytecode instruction by instruction. Each bytecode instruction dispatches to C code that performs the operation. The overhead of that dispatch on every instruction is why pure Python loops are much slower than equivalent C or Rust loops.

Just-in-time (JIT) compilation

A JIT compiler watches the program run and compiles hot code paths — loops and functions called frequently — to native machine code at runtime. V8 (JavaScript), HotSpot (Java), and PyPy (Python) all use JIT compilation.

The JIT can make optimizations that a static compiler cannot: it has real profiling data. If a loop always sees integer arguments, the JIT can compile a version specialized for integers. If that assumption is later violated, the JIT deoptimizes back to interpreted code.

This is why JavaScript can run at speeds competitive with Java in long-running server workloads: V8's JIT eventually compiles the hot paths to highly optimized native code. It is also why JavaScript performance can be inconsistent: changing a variable's type in a loop defeats the JIT's assumptions and triggers deoptimization.

What this means in practice

Knowing the pipeline clarifies several practical questions. Compile-time errors (type errors, undefined variable references in statically typed languages) are caught before deployment; runtime errors are not. Bytecode-based languages pay an interpreter overhead that native-compiled languages avoid. JIT compilation amortizes that overhead for hot code but adds a warmup period and unpredictable optimization cliffs. Understanding these trade-offs guides choices about which language is appropriate for a performance-sensitive task and helps read the performance profiles that tell you where to focus optimization effort.