Two phases.
One program.
Every line you write happens twice. Once when the compiler reads it and turns it into a binary, and again, in a different shape, when the CPU actually runs it. The first phase is compile time. The second is runtime. Almost every tradeoff in programming, from type systems to performance to safety, comes down to where the work happens.
Two moments define every program ever written.
The moment it was built.
And the moment it ran.
Most developers blur them together. They think of their code as one continuous thing, from source file to running process. One flow.
It is not.
It is two completely separate phases. With different rules. Different capabilities. Different failure modes. Different costs.
A bug caught at compile time costs you ten minutes.
A bug caught at runtime costs you a crashed server.
A bug caught at runtime in production costs you your users.
A bug caught at runtime in a deployed smart contract on Ethereum costs you everything. The money is already gone. The transaction cannot be reversed.
This is not a minor distinction.
The line between compile time and runtime is the single most important decision in programming language design.
And once you see it, you see it in every language. In every system. In every layer of this curriculum.
What's the difference?
The compiler turns source code into a binary. That phase is called compile time, and it happens once, on a developer's machine, before anything ships.
When someone runs the binary, the OS loads it into memory and the CPU starts executing instructions. That phase is runtime, and it happens every time the program runs.
The same line of source can produce errors in either phase. Which phase it lands in determines who suffers. A compile-time error stays on your laptop. A runtime error reaches a user, possibly at 2 AM, possibly with their data in transit.
Two errors, two phases
// Two errors. Same program structure. Different phases.
fn main() {
// (1) COMPILE-TIME ERROR. Uncomment and the build fails:
//
// let n: u32 = "forty-two";
//
// error[E0308]: mismatched types
// expected `u32`, found `&str`
//
// No binary is produced. The bug never reaches a user.
// (2) RUNTIME ERROR. Compiles fine. Crashes when the program
// actually runs and the value of `i` is finally known.
let arr = [10, 20, 30];
let i = std::env::args().count(); // depends on how it's invoked
println!("{}", arr[i]); // panic if i >= 3
}#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
// (1) COMPILE-TIME ERROR. Uncomment and clang/gcc rejects it:
//
// int n = "forty-two";
//
// error: incompatible pointer to integer conversion
//
// No binary is produced. The bug never reaches a user.
// (2) RUNTIME ERROR. Compiles fine. Reads garbage or crashes,
// depending on what's adjacent in memory at run time.
int arr[] = {10, 20, 30};
int i = atoi(argv[1]); // value is unknown until launch
printf("%d\n", arr[i]); // undefined behaviour if i is OOB
return 0;
}A compile-time error produces no binary. No binary means no machine code. No machine code means the CPU never runs a single instruction from it. The fetch-decode-execute loop you learned on page 5 never starts. The bug is stopped before silicon. ← see: CPU · Binary
What about an interpreter?
A pure interpreter (Python, Ruby, Bash) skips the up-front compile step. It reads source code and executes it directly. There's still a parse phase that runs before execution, but it happens on the user's machine, every time the program runs. So a typo that a Rust or C compiler catches before shipping shows up in Python only when the line is actually reached at runtime.
This is the same compile/runtime split, drawn at a different point. Less compile-time work means more runtime risk.
Which phase? You decide.
What gets decided when?
Some things have to wait for runtime: user input, network responses, file contents, the current time, random numbers. Anything that depends on the world.
But a surprising amount of work can be done at compile time, if the language lets the compiler do it. The trend in modern systems languages is to push more and more decisions earlier, because:
- Compile-time checks catch bugs before users see them. Every type error caught by the compiler is a runtime crash that never happened.
- Compile-time computation is free at runtime. The work already happened on the developer's machine.
- Compile-time-known sizes and types let the compiler pick efficient code. Stack-allocated, inlined, and specialised paths only work when the shape is fixed before the program runs.
| question | compile time | runtime |
|---|---|---|
What's the type of x? | Yes (Rust, C, Java, Go) | Yes (Python, Ruby, JS) |
What's the size of x? | If it's a primitive or fixed array | If it's Vec, String, malloc'd |
Where does x live in memory? | Stack offsets, static addresses | Heap allocations |
Which function does foo() call? | Direct calls, generics | Function pointers, vtables |
What's 10 * 60 * 60? | Constant folded into 36000 | Recomputed every call |
What's read_user_input()? | Can't know | Whatever the user typed |
Look at the memory row in that table. Stack offsets are compile time. Heap allocations are runtime. The variables page showed exactly this split: fixed-size types go on the stack because the compiler knows their size; dynamic types go on the heap because only runtime knows how big they are. Compile vs runtime is the reason stack and heap exist at all. ← see: Variables · Memory
Computing things at compile time
Most languages now let you tell the compiler "run this for me, please, while you're building." Rust calls these const fn. C++ has constexpr. C has constant expressions and the preprocessor. The result is the same in every case: the value gets baked into the binary, and there's literally nothing for the CPU to compute when the program runs.
// `const fn` lets the compiler run the function at build time.
// The result becomes a baked-in constant, computed once, ever.
const fn factorial(n: u32) -> u64 {
let mut acc: u64 = 1;
let mut i: u32 = 1;
while i <= n {
acc *= i as u64;
i += 1;
}
acc
}
// FACT_10 is computed while the compiler is running on your laptop.
// At runtime, this is just `mov eax, 3628800`. No loop. No work.
const FACT_10: u64 = factorial(10);
fn main() {
println!("baked at compile time: {FACT_10}");
// Same function, called with a value from the world.
// The compiler can't know it, so the loop runs at runtime.
let n: u32 = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(10);
println!("computed at runtime: {}", factorial(n));
}#include <stdio.h>
#include <stdlib.h>
// C has no general "run this function at compile time" feature, but
// constant expressions and the preprocessor cover the simple cases.
// Most compilers will fold this whole expression into a single number.
enum {
FACT_10 = 1*2*3*4*5*6*7*8*9*10 // 3628800, computed at compile time
};
unsigned long long factorial(unsigned n) {
unsigned long long acc = 1;
for (unsigned i = 1; i <= n; i++) acc *= i;
return acc;
}
int main(int argc, char **argv) {
printf("baked at compile time: %d\n", FACT_10);
unsigned n = argc > 1 ? (unsigned)atoi(argv[1]) : 10;
printf("computed at runtime: %llu\n", factorial(n));
return 0;
}The OS row in the table shows static linking as compile time, dynamic linking as runtime. When Bitcoin Core starts it dynamically links to libssl and libc. Those addresses are resolved at runtime by the dynamic linker. The OS page showed how program startup works. This is the compile vs runtime split inside the loader itself. ← see: Operating System
Why this ties to every other page
Look back at every layer this site has covered. Each one has a compile-time / runtime split somewhere, and once you see the pattern it shows up everywhere.
| layer | compile time | runtime |
|---|---|---|
| Number systems | 0xCAFEBABE is parsed and stored as bytes | Bytes are loaded and used; the prefix is gone |
| Binary | Instructions encoded into byte patterns | CPU decodes and executes them |
| ASCII | "hello" baked into rodata | Bytes loaded, sent to a file descriptor, drawn |
| Logic gates | Gate layout fixed at fab time (the chip's compile) | Current flows through that fixed structure |
| CPU | ISA: which bit patterns mean which operations | CPU runs the patterns the compiler chose |
| Memory | Stack offsets, sizes of primitives | Heap allocations, dynamic sizes |
| Operating system | Static linking, syscall numbers fixed in libc | Dynamic linking, page faults, scheduling |
| Variables | "Is the size known?" If yes: stack | "What's the value?" Always runtime |
The whole stack runs on this single distinction. Layer above layer, each one freezes some decision at build time and defers the rest to runtime.
Static vs dynamic dispatch
The classic example of a tradeoff that lives exactly on this line: how does the program decide which function to call?
- Static dispatch picks the function at compile time. The call site jumps directly to the right address. Fast, inlinable, but the binary contains a copy of the function for every concrete type that uses it.
- Dynamic dispatch picks the function at runtime. The call site reads a pointer (a vtable entry, a function pointer) and jumps through it. Slightly slower per call, but the binary stays small and the same code handles any type that follows the contract.
Same problem, two timings, different tradeoffs.
// STATIC dispatch (compile time): the compiler stamps out one
// version of `area` for every concrete shape that calls it.
// Each call is a direct jump to a known function.
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }
fn area_static<S: Shape>(s: &S) -> f64 { s.area() } // resolved at build
// DYNAMIC dispatch (runtime): one copy of `area_dyn`. At runtime,
// each call follows a vtable pointer to figure out which method to run.
fn area_dyn(s: &dyn Shape) -> f64 { s.area() } // resolved at run
fn main() {
let c = Circle { r: 1.0 };
let s = Square { side: 1.0 };
// The compiler emits two area_static specialisations.
// Each call below is as fast as a direct function call.
println!("{}", area_static(&c));
println!("{}", area_static(&s));
// One area_dyn function, two indirect calls. A few cycles slower
// per call, but only one binary copy of the code.
let shapes: Vec<&dyn Shape> = vec![&c, &s];
for s in &shapes {
println!("{}", area_dyn(*s));
}
}// STATIC dispatch: direct function calls, resolved by the linker.
#include <stdio.h>
double circle_area(double r) { return 3.14159 * r * r; }
double square_area(double side) { return side * side; }
// DYNAMIC dispatch: function pointers. The address of the function
// to call is decided at runtime, by reading the pointer.
typedef double (*area_fn)(void *self);
typedef struct { double r; } Circle;
typedef struct { double side; } Square;
double circle_area_fn(void *self) { return circle_area(((Circle*)self)->r); }
double square_area_fn(void *self) { return square_area(((Square*)self)->side); }
typedef struct { void *self; area_fn area; } Shape;
int main(void) {
Circle c = { 1.0 };
Square s = { 1.0 };
// Static: the linker hard-wires the call. No indirection.
printf("%f\n", circle_area(c.r));
printf("%f\n", square_area(s.side));
// Dynamic: the call goes through a pointer. Cheap, but the
// CPU's branch predictor has to guess the target every call.
Shape shapes[] = {
{ &c, circle_area_fn },
{ &s, square_area_fn },
};
for (int i = 0; i < 2; i++)
printf("%f\n", shapes[i].area(shapes[i].self));
return 0;
}Three families of execution model
Faster languages do more work at compile time, in exchange for less flexibility at runtime. More dynamic languages defer almost everything to runtime, in exchange for the ability to redefine code, change types, or load new modules on the fly. There's no winner; the right answer depends on what you're building.
Rust is ahead-of-time compiled. When you write a Rust Bitcoin node, the entire program is compiled to native machine code before it ships. No JIT warmup. No interpreter overhead. The CPU runs your instructions directly. This is why Rust is replacing C in security-critical infrastructure: AOT compilation means the compiler has already done the safety checks before a single packet arrives. ← see: Blockchain · Pointers
What pushing decisions earlier actually buys
- Safety. Bugs caught at compile time never reach a user. Rust's borrow checker is the extreme version of this idea.
- Speed. Work done once at build time costs nothing at run time. Constant folding, inlining, generics specialisation.
- Predictability. Static layouts and static dispatch eliminate whole classes of "it depends on the data" surprises.
- Smaller attack surface. No
eval, no dynamic loading, no surprises means fewer ways for an attacker to inject behaviour the compiler didn't see.
The whole site, framed by time
- Build time: a chip designer writes Verilog. The fab "compiles" it into transistor masks. The masks become silicon.
- Build time: a compiler reads your source. It decides types, sizes, addresses, instructions, and emits a binary.
- Build time: a linker stitches your binary together with libc, baking in syscall numbers and resolving symbol addresses.
- Run time: the OS loads the binary into a fresh virtual address space.
- Run time: the CPU starts executing instructions. Every fetch-decode-execute cycle is one step in the loop the compiler set up.
- Run time: page faults map virtual pages to physical RAM. The allocator hands out heap regions on demand.
- Run time: the program reads input, makes decisions, writes output. The world enters the picture for the first time.
Bitcoin: compile time meets consensus
Bitcoin has one of the most consequential compile-time / runtime splits in the history of software.
The consensus rules are compile time
Bitcoin's consensus rules are hardcoded in every node's binary at compile time. These rules never change at runtime:
- Maximum block size: 4 MB (weight)
- Block reward halving schedule
- SHA-256 proof-of-work requirement
- ECDSA signature validation
- Script opcode definitions
- Maximum number of coins: 21 million
These are not configuration. They are not parameters. They are constants baked into the binary when Bitcoin Core is compiled.
/* Same constants in Rust -
* const values evaluated at compile time */
const MAX_BLOCK_WEIGHT: u32 = 4_000_000;
const COIN: u64 = 100_000_000;
const MAX_MONEY: u64 = 21_000_000 * COIN;
const COINBASE_MATURITY: u32 = 100;
const WITNESS_SCALE_FACTOR: u32 = 4;
/* Compile-time assertion: catches
* any accidental change to the cap */
const _: () = assert!(
MAX_MONEY == 2_100_000_000_000_000,
"MAX_MONEY must equal exactly 21 million BTC"
);
/* These compile-time constants define
* what it means to be a Bitcoin node.
* Not configuration. Not parameters.
* The protocol itself. *//* Bitcoin consensus constants -
* compile-time, never runtime variables */
#define MAX_BLOCK_WEIGHT 4000000UL
#define COIN 100000000ULL /* 1 BTC in satoshis */
#define MAX_MONEY (21000000ULL * COIN)
#define COINBASE_MATURITY 100
#define WITNESS_SCALE_FACTOR 4
/* These are evaluated at compile time.
* No runtime branch. No configuration file.
* If you change these you change Bitcoin.
* You are now on a different blockchain. */
static_assert(MAX_MONEY == 2100000000000000ULL,
"MAX_MONEY must equal 21 million BTC in satoshis");Block validation is runtime
Whether a specific block satisfies those compile-time rules is checked at runtime, when the block arrives over the network.
fn validate_block(
header: &BlockHeader,
transactions: &[Transaction],
) -> Result<(), ValidationError> {
// Runtime: hash this specific block
let hash = double_sha256(header);
// Runtime check against compile-time target
if !hash.meets_difficulty_target(header.bits) {
return Err(ValidationError::InsufficientWork);
}
// Runtime check against compile-time constant
let weight: u32 = transactions.iter()
.map(|tx| tx.weight())
.sum();
if weight > MAX_BLOCK_WEIGHT { // compile-time const
return Err(ValidationError::BlockTooHeavy);
}
// Runtime: verify every signature
for tx in transactions {
tx.verify_signatures()?;
}
Ok(())
}
/* The split is clean:
* What IS a valid Bitcoin block: compile time.
* Whether THIS block IS valid: runtime.
*
* Change the compile-time rules and you fork Bitcoin.
* Every node on the old binary rejects your blocks.
* That is what a hard fork is: changing compile-time
* consensus rules in a way old binaries reject. */typedef struct {
uint32_t version;
uint8_t prev_hash[32];
uint8_t merkle_root[32];
uint32_t timestamp;
uint32_t bits;
uint32_t nonce;
} BlockHeader;
/* Runtime validation: called when a
* new block arrives over the network */
int validate_block(const BlockHeader *header,
const uint8_t *txdata,
size_t txdata_len)
{
/* 1. Check proof of work (runtime) */
uint8_t hash[32];
double_sha256((uint8_t*)header,
sizeof *header, hash);
if (!meets_target(hash, header->bits))
return 0; /* rejected at runtime */
/* 2. Check block weight (runtime) */
uint32_t weight = compute_weight(txdata,
txdata_len);
if (weight > MAX_BLOCK_WEIGHT) /* compile-time const */
return 0; /* runtime check vs compile constant */
/* 3. Validate all transactions (runtime) */
return validate_transactions(txdata, txdata_len);
}Smart contracts: runtime all the way down
Ethereum smart contracts invert this. The EVM (Ethereum Virtual Machine) is an interpreter. Smart contract code is bytecode that executes at runtime, every time a transaction calls a function. This means:
- Bugs that Rust would catch at compile time show up only when someone calls the contract, with real money on the line.
- The DAO hack was a runtime reentrancy bug that a compile-time check would have caught. $60 million drained. Irreversible. A hard fork was needed to recover.
Compile-time bugs cost ten minutes. Runtime bugs in smart contracts cost everything.
Solidity is adding more compile-time checks. Move (Sui, Aptos) was designed with formal verification and compile-time resource ownership specifically because of the DAO. Rust-based smart contracts (Solana, CosmWasm) bring the borrow checker to on-chain code.
The entire evolution of smart contract language design is the story of moving checks from runtime to compile time. Because at runtime, the money is already moving.
Where to dig in next
The compile-time / runtime split shows up under a hundred different names. A few rabbit holes worth following:
- Type systems, especially Hindley-Milner and dependent types, push more invariants into compile time.
- Macros and metaprogramming let user code run at compile time. Rust macros, C++ templates, Lisp macros are three very different takes.
- JIT compilation, especially the V8 and HotSpot designs, blends the two phases by recompiling hot code while the program runs.
- Partial evaluation is the formal study of moving computation between phases. The "Futamura projections" are the classic result.
- Profile-guided optimisation goes the other direction: feed runtime data back into the compiler for the next build.
Every one of those is a different way to shift work along the same line.
Compile vs Runtime across ScrapyBytes
The same ideas surface all over ScrapyBytes. Here is where this page connects to the rest of the curriculum, and how to follow each thread.
Compile time produces machine instructions; the CPU is runtime, executing them. The boundary on this page is the boundary at the CPU's front door.
scrapybytes.vercel.app/cpu →VariablesA stack variable's offset is fixed at compile time; a heap allocation's address is only known at runtime. The variables page is the cleanest example of this split.
scrapybytes.vercel.app/variables →MemoryStack layout is decided by the compiler; heap allocation happens while the program runs. The memory page's two regions map onto this page's two phases.
scrapybytes.vercel.app/memory →PointersRust moves whole classes of pointer bugs from runtime crashes to compile-time errors. The pointers page is full of mistakes this page decides when to catch.
scrapybytes.vercel.app/pointers →Operating SystemStatic linking is compile time; the dynamic linker resolves addresses at startup. The OS page's loader is this page's split running inside the operating system.
scrapybytes.vercel.app/operating-system →BinaryA compiler turns source text into a binary of machine code. No compile, no binary, no bits for the CPU to run. The binary page is this page's output.
scrapybytes.vercel.app/binary →Big O NotationSome costs are paid once at compile time, others on every run. Knowing which is a Big O question about where the work lives. The big-o page is the lens.
scrapybytes.vercel.app/big-o →Number SystemsA literal like 0xCAFEBABE is parsed to bytes at compile time and the hex prefix vanishes; the CPU runs the bytes at runtime. The number systems page is the notation that disappears.
A chip's layout is fixed at fabrication, its compile time, and current flows through that frozen silicon at runtime. The logic gates page is hardware that cannot change while running.
scrapybytes.vercel.app/logic-gates →ArraysFixed array sizes and literal-index bounds are compile time; Vec sizes and variable indices are runtime. The arrays page is this split applied to indexing.
Whether a call is tail-recursive is compile time; whether it overflows the stack is always runtime, decided by the base case. The recursion page is that runtime check.
scrapybytes.vercel.app/recursion →HashingSHA-256's rounds and constants are fixed at compile time; which block you hash is runtime. The hashing page is a known algorithm fed unknown data.
scrapybytes.vercel.app/hashing →BlockchainBitcoin's consensus rules are compile-time constants in every node binary; validating a given block is runtime. The blockchain page is rules versus validation, and changing the rules forks the chain.
scrapybytes.vercel.app/blockchain →Distributed SystemsWhether you are partitioned right now is only knowable at runtime, but which CAP tradeoff you accept is an architectural decision made before shipping. The distributed systems page is that choice.
scrapybytes.vercel.app/distributed-systems →Sorting AlgorithmsThe sort you choose is compile time; how many comparisons it makes on this input is runtime. The sorting page is why nearly-sorted data wins in ways Big O cannot see.
scrapybytes.vercel.app/sorting →