root.system / 0x0A / time

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.

Beginner// level 01

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

Rust• • •
// 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
}
C• • •
#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;
}
// the rule of thumb
A compile-time error is the compiler refusing to build the binary at all. A runtime error is the binary running and then misbehaving (panicking, crashing, returning the wrong answer, or, in C, silently corrupting memory). The first kind is cheap; the second kind is expensive.

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.

// which phase? you decide.
scenario 1 / 10
0 correct
Type mismatch
let x: u32 = "hello";

Assigning a string literal to an integer variable.

ten scenarios, shuffled each round. classify each as compile time or runtime, then read why. the line between the two phases is the most important decision in language design.

Intermediate// level 02

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:

  1. Compile-time checks catch bugs before users see them. Every type error caught by the compiler is a runtime crash that never happened.
  2. Compile-time computation is free at runtime. The work already happened on the developer's machine.
  3. 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.
questioncompile timeruntime
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 arrayIf it's Vec, String, malloc'd
Where does x live in memory?Stack offsets, static addressesHeap allocations
Which function does foo() call?Direct calls, genericsFunction pointers, vtables
What's 10 * 60 * 60?Constant folded into 36000Recomputed every call
What's read_user_input()?Can't knowWhatever 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.

Rust• • •
// `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));
}
C• • •
#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 line is movable
A C compiler doing constant folding moves arithmetic from runtime to compile time. A Rust compiler doing trait specialisation moves dispatch from runtime to compile time. A type system moves "is this operation valid?" from runtime ("would have crashed") to compile time ("won't compile"). Every modern compiler is, in part, a machine for moving work earlier.

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

Advanced// level 03

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.

layercompile timeruntime
Number systems0xCAFEBABE is parsed and stored as bytesBytes are loaded and used; the prefix is gone
BinaryInstructions encoded into byte patternsCPU decodes and executes them
ASCII"hello" baked into rodataBytes loaded, sent to a file descriptor, drawn
Logic gatesGate layout fixed at fab time (the chip's compile)Current flows through that fixed structure
CPUISA: which bit patterns mean which operationsCPU runs the patterns the compiler chose
MemoryStack offsets, sizes of primitivesHeap allocations, dynamic sizes
Operating systemStatic linking, syscall numbers fixed in libcDynamic 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.

Rust• • •
// 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));
    }
}
C• • •
// 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

01 / ahead-of-time
C, Rust, Go
All compilation happens before shipping. The binary is final, native, and runs at full hardware speed. Errors caught early; flexibility low.
02 / just-in-time
Java, JS, .NET
Some compilation happens at startup or while running. Profile-guided optimisations adapt to the real workload. A warm-up cost in exchange for cross-platform binaries and runtime adaptability.
03 / interpreted
Python, Bash
No precompiled native code at all. The interpreter reads source (or bytecode) and executes line by line. Maximum flexibility (redefine code on the fly), maximum runtime cost.

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

// the four wins of compile-time work
  1. Safety. Bugs caught at compile time never reach a user. Rust's borrow checker is the extreme version of this idea.
  2. Speed. Work done once at build time costs nothing at run time. Constant folding, inlining, generics specialisation.
  3. Predictability. Static layouts and static dispatch eliminate whole classes of "it depends on the data" surprises.
  4. 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

// from electrons to executable, with the clock running
  1. Build time: a chip designer writes Verilog. The fab "compiles" it into transistor masks. The masks become silicon.
  2. Build time: a compiler reads your source. It decides types, sizes, addresses, instructions, and emits a binary.
  3. Build time: a linker stitches your binary together with libc, baking in syscall numbers and resolving symbol addresses.
  4. Run time: the OS loads the binary into a fresh virtual address space.
  5. Run time: the CPU starts executing instructions. Every fetch-decode-execute cycle is one step in the loop the compiler set up.
  6. Run time: page faults map virtual pages to physical RAM. The allocator hands out heap regions on demand.
  7. 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.

Rust• • •
/* 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. */
C• • •
/* 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.

Rust• • •
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. */
C• • •
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);
}
// the split is clean
What IS a valid Bitcoin block is compile time: the rules in the binary. Whether THIS block IS valid is runtime: this specific block, with these specific transactions, checked against those rules. Change the compile-time rules and you fork Bitcoin. Every node running the old binary rejects your blocks. That is exactly what a hard fork is.

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.

CPU

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
Variables

A 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
Memory

Stack 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
Pointers

Rust 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 System

Static 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
Binary

A 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 Notation

Some 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 Systems

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

scrapybytes.vercel.app/number-systems
Logic Gates

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
Arrays

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

scrapybytes.vercel.app/arrays
Recursion

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
Hashing

SHA-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
Blockchain

Bitcoin'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 Systems

Whether 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 Algorithms

The 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
next up / 0x0B
Houses on a numbered street: arrays from first principles.
arrays