Why your program won't run on my machine
Every download page asks you the same quiet questions. Mac or Windows? Intel or Apple Silicon? You've answered them so many times you don't notice anymore. But sit with it for a second and it's strange. The app is finished software. It exists. Why does it matter what machine it lands on? Meanwhile the same afternoon, someone emails you a Python script written on a five-year-old Windows laptop and it runs on your Mac without a single question being asked. By the end of this part, the finale of the series, both facts will feel obvious.
Machine code is a dialect
From part 3: an ahead-of-time compiler's last act is producing machine code, instructions in the native format of a specific CPU family. The key word is specific. x86-64 chips, the Intel and AMD kind, speak one instruction format. ARM64 chips, the Apple Silicon and phone kind, speak a completely different one. Different encodings, different registers, different everything. "Add these two numbers" isn't one universal sentence in silicon. Each family spells it its own way.
So a binary compiled for x86-64 is, to an Apple Silicon chip, not slow or awkward. It's gibberish. The proof this wall is real: when Apple switched Mac chips from Intel to ARM, they had to ship Rosetta 2, an entire translation layer whose only job is converting x86-64 machine code into ARM64 machine code so old apps could keep running. When the vendor has to build a live translator between its own computers, you know machine code doesn't travel.
Same chip, still won't run
Here's the part that surprised me more. Take two machines with the same CPU, a Linux box and a Mac both on ARM64, and a binary built for one still won't run on the other. If machine code were the whole story, it should. It isn't, because what you ship isn't loose machine code. It's an executable, and an executable is a package.
Inside that package: the machine code, yes, but also your program's baked-in data, a table naming every library it expects to find at runtime, and metadata telling the operating system where everything sits and where execution should begin. And the package format itself is an OS convention: Linux wants ELF, macOS wants Mach-O, Windows wants PE, the thing behind .exe. Before your program runs, the OS loader has to parse this package, wire up the libraries, and set the whole thing up in memory. Hand macOS an ELF file and it isn't confused about the machine code inside. It never gets that far. It's like receiving a package addressed in a postal system your country doesn't use.
And it goes deeper than packaging. A running program constantly asks its operating system for things: open this file, give me memory, send these bytes. Each OS offers different services, spelled differently, through different libraries. Your binary isn't just built for a CPU. It's built into a standing relationship with one operating system, and that relationship is baked in at compile time.
Reading a target triple
All of this is why "compiled" always secretly means "compiled for somewhere," and why Rust names its build targets the way it does. x86_64-unknown-linux-gnu reads as: x86-64 CPU, no particular vendor, Linux, GNU-flavored system libraries. aarch64-apple-darwin: ARM64 CPU, Apple, Darwin, which is the OS family under macOS. Every part of that string is one of the walls we just met. Architecture, platform, operating system, library conventions. A "target" is the full address of a place software can live, and a native binary is compiled for exactly one address.
The two ways out
So how does anyone ship software to more than one address? Everything in this series funnels into this one fork. There are exactly two strategies, and you already know both.
Strategy one, rebuild for every address. That's Rust, C, Go. Cross the walls at compile time: build once per target, ship a native binary to each, and every copy runs at full speed with no baggage. The cost is on you, the builder: N platforms, N builds, and the download page has to ask its little questions.
Strategy two, move the problem to the other side. That's the bytecode play from part 4, and now you can see what it's really for. Compile to instructions for an imaginary machine, and ship that one artifact everywhere. The walls didn't go anywhere. They got handled by whoever built the VM for each platform, once, so that every developer after them could skip it. "Write once, run anywhere" decoded: we already compiled the hard part for every machine, so you don't have to. And your Python script that ran without questions? Same trick. The person who installed Python on each machine installed the per-platform part. Your .py file rides on top, portable because it never gets closer to the metal than CPython does.
The modern remix of this idea is WebAssembly: a bytecode designed to run in every browser, and increasingly everywhere else. And who's the most enthusiastic adopter? Rust, the poster child of ahead-of-time compilation, happily compiling to bytecode for a virtual machine. The categories from the tutorial aren't just leaky at this point. The "compiled language" ships bytecode and the "interpreted languages" run JIT compilers. Everyone is borrowing everyone's strategy, because they were always just strategies.
The model you should have been taught
That's the bottom of the rabbit hole, so let's climb back up and count what we found. A linter isn't a worse compiler, it answers a different question (part 1). Your file is characters, and everything you think of as code is a structure the toolchain rebuilds from text every time (part 2). "Compiling" is a pipeline of lowerings through in-between forms, not one arrow (part 3). Bytecode is machine code for an imaginary machine, and VMs, interpreters, and JITs are the machinery that makes it real (part 4). Languages are specs, implementations have strategies, and "compiled versus interpreted" was a question aimed at the wrong layer (part 5). And machine code is mail with a very specific address on it, which is why the whole portability circus exists at all (this one).
The old two-bucket taxonomy wasn't evil. It was a beginner abstraction, and beginner abstractions are fine. The failure is that nobody ever comes back to tell you where the abstraction stops being true, so it quietly hardens into a fact, and then every real thing you meet later, __pycache__, JIT warmup, "download for Apple Silicon," WebAssembly, feels like an exception to a rule instead of what it actually is: the actual system, showing through.
I started this series embarrassed to ask why a type-safe language needs a linter. Every question after that one sounded dumber and turned out to be more fundamental. I'm pretty sure that's not a coincidence. The questions that feel too basic to ask out loud are usually load-bearing, and the tutorials skipped them precisely because they're hard to answer in a paragraph. So here's six essays instead. If someone you know is still filing languages into two buckets, send them to the start.