CS302 Episode 4: Interpretation vs Compilation, or Two Ways to Turn Your Intentions Into Consequences

Deep Dream Generator

You write a line of code.

You press Run.

Something happens.

This sequence feels perfectly reasonable until you ask the dangerous question:

What happened between the second and third steps?

Your computer cannot directly obey Python, JavaScript, Java, C, Rust, or the strongly worded comments you left for whoever maintains the project next. A processor ultimately works with machine instructions encoded for a particular architecture.

Something must therefore transform your source code into operations the machine can execute.

Traditionally, we describe the two main approaches as interpretation and compilation.

An interpreter executes a program through another program.

A compiler translates a program from one representation into another.

That distinction is useful. It is also nowhere near the whole story.

Modern language implementations compile code, interpret intermediate instructions, compile parts again while the program runs, cache the results, optimize frequently used paths, and occasionally change their minds when reality disproves an earlier assumption.

The old question was:

Is this language interpreted or compiled?

The better question is:

What transformations happen, when do they happen, and what executes the result?

That question takes us backstage, where the machinery is considerably more interesting and only slightly less chaotic.

First, Separate the Language From Its Implementation

A programming language is a set of rules describing valid programs and what those programs mean.

An implementation is the software that makes those rules operational.

This matters because languages are not inherently interpreted or compiled.

Python is a language.

CPython is one implementation of Python.

JavaScript is a language.

V8, SpiderMonkey, and JavaScriptCore are implementations of JavaScript.

C is a language.

GCC and Clang are compilers that implement it.

The same language may have several implementations using different execution strategies. One may interpret source directly. Another may compile to bytecode. A third may generate native machine code.

Calling Python “interpreted” is acceptable as beginner shorthand. Treating that description as the complete architecture is where the furniture begins sliding downhill.

The Basic Compiled Model

In the simplest compiled model, a compiler receives source code and produces another program before execution begins.

Suppose we write this small C program:

#include <stdio.h>

int main(void) {
int price = 7;
int quantity = 3;
printf("%d\n", price * quantity);
return 0;
}

We might compile it with:

gcc total.c -O2 -o total

The compiler analyzes the source, checks applicable language rules, transforms the program, performs optimizations, and produces an executable named total.

We can then run it:

./total

The processor does not execute the original C statements. It executes machine instructions produced from them.

Conceptually:

C source code → compiler → native executable → processor

The important word is conceptually. A real compiler may use several internal representations, invoke an assembler, call a linker, include runtime libraries, and perform enough additional work to make that neat arrow look rather optimistic.

We will examine those stages properly in Episode 5.

The Basic Interpreted Model

In the simplest interpreted model, an interpreter reads a program representation and performs the requested operations while the program runs.

Consider this Python code:

price = 7
quantity = 3
print(price * quantity)

You can execute it with:

python total.py

The Python interpreter remains part of the execution process. It manages objects, evaluates operations, handles function calls, tracks exceptions, and coordinates the runtime environment.

Conceptually:

Python source code → interpreter → behavior

This looks direct, but even here the source usually does not travel straight from text to execution one line at a time.

The implementation still needs to understand the program’s structure.

That brings us back to CS302 Episode 2: Parsing and Structure. Tokens become structured syntax, expressions gain relationships, and the implementation determines whether the program is grammatically valid.

Interpretation does not eliminate parsing.

It merely changes what happens after parsing.

Interpreters Usually Do Not Read One Line and Immediately Obey It

A common mental model says an interpreter reads line one, executes line one, reads line two, and continues downward until something catches fire.

Some simple interpreters work approximately that way.

Many important ones do not.

An interpreter may first:

  1. Tokenize the source.
  2. Parse it into a tree.
  3. Perform semantic checks.
  4. Compile the tree into bytecode.
  5. Execute that bytecode inside a virtual machine.

That is exactly the sort of hybrid behavior that makes the interpreted-versus-compiled label wobble.

CPython, for example, compiles Python source into bytecode and then executes that bytecode using its interpreter. Python’s official dis module documentation lets you inspect those instructions.

Try this:

import dis

def calculate_total(price, quantity):
return price * quantity
dis.dis(calculate_total)

The output varies by Python version, but you will see bytecode operations representing tasks such as loading local values, performing an operation, and returning the result.

Your function was compiled.

It is also interpreted.

Nobody broke the rules. The rules were simply never as binary as the slogan suggested.

Bytecode: The Useful Middle Ground

Bytecode is a lower-level instruction format designed for a software-based virtual machine rather than a physical processor.

Instead of translating directly from source code to ARM or x86 machine instructions, an implementation may produce instructions for an abstract machine.

Conceptually:

Source code → bytecode compiler → bytecode → virtual machine

This approach offers several benefits.

NightCafe

Portability

The same bytecode can run anywhere a compatible virtual machine exists.

Java famously uses this model. Java source is commonly compiled into class files containing instructions for the Java Virtual Machine. A suitable JVM implementation then runs those instructions on the host system.

The bytecode remains portable because the virtual machine handles the unpleasant local details involving operating systems and processor architectures.

Faster repeated execution

Parsing and analyzing source code takes time. If an implementation can save bytecode, it may avoid repeating some of that work.

A simpler execution target

A language implementation can target one virtual instruction set instead of immediately generating native instructions for every supported processor.

The cost is that the virtual machine itself must execute or further compile those instructions.

There is always machinery somewhere. Software engineering mainly decides where to place it and who must maintain it.

Ahead-of-Time Compilation

When compilation happens before the program is launched, it is commonly called ahead-of-time compilation, or AOT.

C, C++, Rust, and Go commonly use AOT compilation to produce native executables.

The advantages can include:

  • Fast startup
  • Strong opportunities for whole-program analysis
  • Predictable deployment artifacts
  • No need for a full compiler during ordinary execution
  • Direct execution of native machine instructions

The tradeoffs can include:

  • Separate builds for different platforms or processor architectures
  • A noticeable build step
  • Less access to actual runtime behavior during optimization
  • Larger or more complicated build pipelines
  • Platform-specific dependencies hiding in places nobody remembered to document

AOT compilation is especially attractive for command-line tools, embedded systems, operating-system components, performance-sensitive services, and applications where startup latency matters.

If a serverless function spends 80 milliseconds doing useful work and 600 milliseconds preparing to do it, the user will not be comforted by an elegant language taxonomy.

Just-in-Time Compilation

A just-in-time compiler, or JIT compiler, generates machine code while the program is running.

Why wait until runtime?

Because runtime has evidence.

It can observe:

  • Which functions execute frequently
  • Which branches are usually taken
  • What types actually appear
  • Which objects share common shapes
  • Where the program spends most of its time

A JIT compiler can use that information to optimize the hottest parts of the program.

This creates a common tiered strategy:

  1. Start quickly using an interpreter or basic compiled form.
  2. Watch the running program.
  3. Identify frequently executed code.
  4. Compile that code into faster machine instructions.
  5. Replace earlier assumptions if program behavior changes.

JavaScript engines provide a particularly useful example. V8 can begin with bytecode executed by its Ignition interpreter, then use compilation tiers such as the Sparkplug baseline compiler as the program runs.

That is not pure interpretation.

It is not traditional compile-once execution either.

It is adaptive execution: begin cheaply, gather evidence, and spend optimization effort where it appears worthwhile.

JIT compilation can deliver excellent peak performance, but it also adds complexity, compilation overhead, memory use, warm-up time, and the occasional performance surprise caused by an optimization being withdrawn.

The program may become faster after running for a while.

It may also become slower after the runtime discovers that a confident assumption was built on sand.

Grok

One Tiny Expression, Several Journeys

Consider:

total = price * quantity

In an AOT-compiled native program, the compiler may determine representations for the values, select machine instructions, allocate registers, and place the resulting instructions into an executable.

In a bytecode interpreter, the expression may become instructions resembling:

LOAD price
LOAD quantity
MULTIPLY
STORE total

The interpreter repeatedly examines those bytecode operations and performs them.

In a JIT-based runtime, the expression may begin in bytecode. If it runs often enough, the runtime may compile it into native code using observations about the values involved.

The source expression stays the same.

The execution journey changes dramatically.

This is why CS302 Episode 1: What a Programming Language Is separated the language from the machinery implementing it. Syntax tells us how the expression is written. Semantics tells us what multiplication means. The implementation decides how to make that meaning happen.

CS302 Episode 3: Type Systems and Meaning adds another layer. If operand types are known before execution, a compiler may select operations early. If they are discovered at runtime, the implementation must inspect values, use specialized representations, or speculate and verify.

Types are not merely declarations. They influence the machinery available beneath them.

Performance: Compiled Does Not Automatically Mean Fast

“Compiled languages are fast, interpreted languages are slow” is a useful sentence if your goal is to begin an argument before lunch.

Performance depends on much more than the label.

It depends on:

  • The quality of the compiler or interpreter
  • The program’s workload
  • Available optimization time
  • Runtime profiling
  • Memory allocation
  • Garbage collection
  • Input and output
  • Libraries and native extensions
  • Hardware behavior
  • Startup requirements
  • Whether the benchmark quietly measures something irrelevant

A native AOT compiler can perform expensive optimization once and reuse the resulting executable many times.

A JIT compiler can optimize using actual runtime evidence unavailable to an AOT compiler.

An interpreter can start quickly because it avoids an expensive compilation phase.

A supposedly interpreted program may spend most of its time inside highly optimized native libraries.

If Python asks a native numerical library to multiply enormous arrays, the loop doing the heavy work may not be interpreted Python at all.

Meanwhile, a beautifully compiled program can still wait on a database query for three seconds.

The processor is not always the bottleneck. Sometimes it is sitting patiently while the network examines its life choices.

Portability: What Exactly Must Travel?

Portability also depends on the execution model.

A native executable normally targets a particular operating system and processor architecture. An x86–64 Linux binary does not automatically become an ARM macOS application through positive thinking.

Source-distributed programs can travel widely, but the destination needs a compatible interpreter, compiler, libraries, and environment.

Bytecode can be portable across systems with compatible virtual machines.

Containers improve environmental consistency, but they do not erase processor architectures or kernel boundaries.

WebAssembly introduces another portable compilation target designed to run inside compatible environments, but even there, portability depends on available host capabilities and interfaces.

The practical question is not merely:

Is the program portable?

It is:

Which artifact is portable, and what must already exist at the destination?

That distinction saves deployment teams from discovering philosophy during an outage.

When Do Errors Appear?

Compilation can catch some problems before the program starts.

A compiler may reject malformed syntax, unresolved names, invalid type combinations, inaccessible members, or violated language rules.

But compilation cannot prove that every program will behave correctly.

This can compile perfectly:

def divide_bill(total, guests):
return total / guests

It still fails when guests is zero.

Static analysis may detect more problems before runtime, especially when the language provides strong type information and the tools are sophisticated. Dynamic runtimes defer more decisions until actual values are available.

Neither approach abolishes testing.

As CS102 Episode 12: Testing and Reliability explained, successful execution once is not persuasive evidence that the program will survive different data, timing, environments, or users.

Compilation checks whether certain rules were satisfied.

Testing checks whether the resulting behavior matches what people actually needed.

Those are related jobs, not interchangeable ones.

A Real-World Choice: A Cloud Function and a Command-Line Tool

Imagine a team building two applications.

The first is a small cloud function that validates uploaded records. It changes frequently, handles modest traffic, and relies on several mature Python libraries.

Using Python may give the team:

  • Rapid development
  • A short edit-run-debug cycle
  • Excellent library access
  • Straightforward experimentation

But the team must also consider interpreter availability, dependency packaging, memory use, and cold-start latency.

The second application is a command-line tool distributed to thousands of machines. Users expect one file that launches instantly without installing a runtime.

A language that produces a native executable may simplify distribution and startup. The build pipeline becomes more involved, and the team must create artifacts for each supported platform.

Neither choice is universally superior.

The cloud function values development speed and ecosystem access.

The command-line tool values packaging simplicity and immediate startup.

Language execution strategy is therefore not academic decoration. It affects deployment size, operational reliability, debugging, patching, observability, and how many messages begin with, “It works on my machine.”

AI Coding Workflows Still Meet the Same Machinery

AI tools can write source code quickly, but they do not exempt that code from language implementation.

An AI coding agent may:

  1. Generate Python source.
  2. Launch an interpreter inside a sandbox.
  3. Observe an exception.
  4. revise the source.
  5. Run tests.
  6. Package the result for production.

That rapid loop benefits from interpretation because the agent can execute small changes immediately.

Production may tell a different story. Native dependencies must still match the target architecture. Bytecode caches may not be portable across interpreter versions. JIT warm-up can affect latency measurements. Dynamically generated code complicates security review. Reproducible builds still matter.

AI makes producing code cheaper.

It does not make execution models optional.

If anything, automated code generation makes it more important to understand what will actually run, what permissions it receives, and which transformations occur before the machine obeys it.

The Historical Split Was Never Entirely Clean

Compilers and interpreters grew from different practical needs, not from rival departments of computer philosophy.

Early programmers worked directly with machine instructions and assembly language. As higher-level languages emerged, programmers needed software capable of translating human-oriented notation into executable form.

Grace Hopper’s early compiler work and the later FORTRAN compiler helped establish that computers could translate higher-level descriptions into machine code. The Computer History Museum’s software and languages timeline traces many of these developments.

Interactive and time-sharing systems created another priority: immediate feedback. Languages such as BASIC became closely associated with entering a command or program and seeing results without waiting through the traditional batch-processing ritual.

Compilation emphasized translation and reusable executable output.

Interpretation emphasized interaction and runtime flexibility.

Modern systems cheerfully borrow from both traditions.

Misconceptions Worth Retiring

“A compiler always produces machine code”

No. A compiler translates from one representation to another.

It may produce machine code, assembly, bytecode, another programming language, an intermediate representation, or code for a virtual machine.

A TypeScript compiler can produce JavaScript. A Java compiler commonly produces JVM bytecode. A source-to-source compiler may translate modern language features into older syntax.

Compilation describes transformation, not one mandatory destination.

Gemini

“An interpreter executes the original source directly”

Sometimes.

Many interpreters execute a parsed tree, bytecode, or another internal representation instead.

The original text has already been transformed.

“Compiled programs have no runtime”

They often do.

Compiled programs may rely on runtime libraries for memory management, exceptions, threading, input and output, reflection, garbage collection, or language-specific services.

The runtime may be smaller or less visible. It has not necessarily vanished.

“Compilation catches all bugs”

It catches bugs detectable through the rules and information available during compilation.

It does not know whether the business requested the right rule, whether the database contains surprising data, or whether two services will race each other at 2:13 a.m.

“Interpreted means portable”

An interpreted program is portable only where a compatible implementation and required dependencies are available.

Source code does not become portable merely because nobody compiled it yet.

“Binaries protect source code”

Compilation may make a program harder to inspect casually, but native executables and bytecode can be analyzed and reverse-engineered.

Compilation is not a security boundary.

The Tradeoff Table Hiding Under the Argument

Interpretation often favors:

  • Fast experimentation
  • Interactive use
  • Runtime flexibility
  • Easier dynamic inspection
  • Shorter edit-run cycles

Ahead-of-time compilation often favors:

  • Fast startup
  • Predictable native deployment
  • More optimization before launch
  • Reduced runtime compilation work
  • Easier distribution as self-contained executables

Just-in-time compilation often favors:

  • Runtime-informed optimization
  • Strong peak performance
  • Adaptive specialization
  • Portable intermediate formats

Its costs may include:

  • Warm-up time
  • Higher runtime complexity
  • Additional memory use
  • Less predictable performance
  • A larger attack surface in systems that generate executable memory

These are tendencies, not laws.

A mature engineering decision considers the workload, deployment environment, team, ecosystem, latency target, debugging requirements, and maintenance horizon.

No single execution model wins every category. That is why every confident one-sentence comparison eventually needs several paragraphs and perhaps a snack.

Where This Fits in the Degree

From CS101, Variables and Conditionals introduced values and decisions, while Loops and Functions introduced repeated execution and reusable behavior. Compilers and interpreters must turn all of those constructs into executable operations.

From CS102, Memory and the Machine showed where program values live, while State, Bugs, and Program Behavior showed why runtime conditions matter. An execution engine must manage both memory and changing state while preserving the language’s semantics.

CS201 Episode 1: How a Computer Actually Runs a Program then brought the view closer to the hardware.

Now CS302 connects those layers to language implementation.

Next, Episode 5 will open the compiler pipeline itself: lexing, parsing, semantic analysis, intermediate representations, optimization, and code generation. If this episode explained the possible journeys, the next one examines the checkpoints along the road.

The Final Distinction

Interpretation and compilation are not opposing identities permanently assigned to languages.

They are techniques.

A compiler transforms a program.

An interpreter performs the operations described by a program representation.

A modern implementation may use both, several times, at different stages, for different parts of the same application.

So the next time someone announces that a language is “interpreted” or “compiled,” you can ask the question that makes the discussion genuinely useful:

Which implementation, which representation, and at what stage?

Then watch the simple label unfold into an actual system.

If this made the machinery behind Run feel a little less mysterious, follow for Episode 5. And leave a comment: which language’s execution model should we take apart next?

Art Prompt (Gothic): An opulent late-medieval procession winding through a steep, fantastical hillside toward a luminous palace courtyard, with anonymous travelers in richly patterned robes, slender horses, elegant hounds, small exotic animals, flowering trees, and distant stone towers layered into a jewel-like landscape. Fill the scene with burnished gold leaf, vermilion, lapis blue, emerald green, rose pink, and warm ivory, using intricate punched-gold halos, embossed borders, delicate botanical detail, and finely observed fabrics that shimmer as though worked by a master jeweler. Arrange the figures in a dense rhythmic sweep that travels from the shadowed lower foreground through rocky paths and clustered attendants toward a radiant focal point beneath ornate Gothic arches. Use flattened perspective, graceful elongated forms, crisp tempera-like contours, tiny narrative details, and an atmosphere of ceremonial splendor, courtly elegance, and enchanted discovery. Museum-quality International Gothic panel painting, refined and family-friendly, no readable text, no logos, no recognizable people, no modern objects.

ChatGPT

Video Prompt: Open with a sudden shower of glittering gold leaves bursting across the frame as the richly dressed procession surges forward along the hillside. Horses toss their decorated bridles, patterned robes ripple in quick rhythmic waves, hounds dart between footsteps, banners snap overhead, and jewel-bright birds sweep past the camera. Let the landscape unfold in lively layered motion as flowering branches curl open, distant towers brighten, and points of embossed gold flash with every beat. Accelerate toward the palace courtyard, where the procession spirals around the radiant arches and the entire scene briefly transforms into a dazzling mosaic of vermilion, lapis blue, emerald, rose, and gold before settling into a perfect illuminated-panel composition. Energetic short-form pacing, elegant transitions, tactile tempera texture, no readable text, no logos, no recognizable people, no modern objects.

Song Recommendations:

Tamacun — Rodrigo y Gabriela
Rumble and Sway — Jamie N Commons

Leave a Comment