DEV Community

Cover image for I Built a Rust Compiler for a 20-Year-Old Mac (Borrow Checker and All)
AutoJanitor
AutoJanitor

Posted on

I Built a Rust Compiler for a 20-Year-Old Mac (Borrow Checker and All)

Modern Rust does not compile for PowerPC Mac OS X Tiger. The newest compiler that runs natively on Tiger is GCC 4.0.1 from 2005. LLVM abandoned PowerPC years ago. The official Rust compiler has never targeted this platform.

So I wrote one.

rust-ppc-tiger is a Rust-to-PowerPC compiler written in C. It parses Rust source code, enforces ownership and borrowing rules, and emits native PowerPC assembly with AltiVec SIMD optimizations. It runs ON Tiger, compiled with that ancient GCC 4.0.1. And it works on real hardware.

$ ./rustc_ppc hello.rs > hello.s
$ as -o hello.o hello.s
$ gcc -o hello hello.o
$ ./hello
Hello from Rust on PowerPC G4!
Enter fullscreen mode Exit fullscreen mode

Test hardware: Power Mac G4 Dual 1.25 GHz, Mac OS X Tiger 10.4.12, 2GB RAM.

This article covers why, how, and what comes next.

The Problem: A Dead Platform That Refuses to Die

Tiger shipped in 2005. Apple abandoned PowerPC in 2006. Every modern tool chain has moved on. But millions of these machines still exist in closets, schools, basements, and labs like mine. They are not junk -- they are 128-bit SIMD machines with a clean RISC architecture that modern ARM borrowed heavily from.

The software situation is grim:

  • No modern TLS: Tiger's OpenSSL is too old for HTTPS. You cannot visit most websites.
  • No modern compilers: GCC 4.0.1 is the ceiling. No C++17, no Rust, no Go.
  • No package managers: Homebrew dropped Tiger. MacPorts barely works.
  • No modern SSH: Tiger ships OpenSSH 4.5 with multiple critical CVEs.

The hardware is fine. The software ecosystem abandoned it. So we are rebuilding the software.

If you want context on why I care about vintage hardware, I wrote about our Proof-of-Antiquity blockchain that rewards old machines and our POWER8 LLM inference server. This is part of that same philosophy: old silicon is not waste.

The Architecture: Rust in, PowerPC Out

The compiler is structured as a set of C source files that each handle a domain of the Rust language. The core is rustc_100_percent.c at 1,205 lines -- it handles parsing, type checking, and code generation for the full Rust type system.

# Build the compiler on Tiger/Leopard with Xcode's gcc
gcc -O3 -mcpu=7450 -maltivec -o rustc_ppc rustc_100_percent.c

# For G5 machines
gcc -O3 -mcpu=970 -maltivec -o rustc_ppc rustc_100_percent.c
Enter fullscreen mode Exit fullscreen mode

The type system maps all Rust types to PowerPC register and stack conventions:

typedef enum {
    TYPE_I8, TYPE_I16, TYPE_I32, TYPE_I64, TYPE_I128,
    TYPE_U8, TYPE_U16, TYPE_U32, TYPE_U64, TYPE_U128,
    TYPE_F32, TYPE_F64, TYPE_BOOL, TYPE_CHAR,
    TYPE_STR, TYPE_STRING, TYPE_VEC, TYPE_ARRAY,
    TYPE_TUPLE, TYPE_STRUCT, TYPE_ENUM, TYPE_REF,
    TYPE_MUT_REF, TYPE_BOX, TYPE_RC, TYPE_ARC,
    TYPE_OPTION, TYPE_RESULT, TYPE_CLOSURE,
    TYPE_FN_PTR, TYPE_SLICE, TYPE_TRAIT_OBJ
} RustType;
Enter fullscreen mode Exit fullscreen mode

Every Rust variable tracks its lifetime, generic parameters, mutability, reference count (for Rc/Arc), and drop chain for RAII:

typedef struct Variable {
    char name[64];
    RustType type;
    int offset;
    int size;
    char lifetime[32];
    char generic_params[128];
    int is_mut;
    int ref_count;
    struct Variable* drop_chain;  // For RAII
} Variable;
Enter fullscreen mode Exit fullscreen mode

The compiler also handles traits with vtable generation, generic monomorphization, impl blocks, closures, modules, and a full macro_rules! expander. This is not a toy subset -- it targets Firefox compilation.

The Borrow Checker: Not Just "C With Fancy Syntax"

Without a borrow checker, a Rust compiler is just C with extra steps. The borrow checker (rustc_borrow_checker.c, 500+ lines) is the part that makes this a real Rust implementation.

It enforces the three rules:

  1. Each value has exactly one owner
  2. When the owner goes out of scope, the value is dropped
  3. You can have EITHER one mutable reference OR any number of immutable references -- but not both
typedef enum {
    OWNER_OWNED,        /* Variable owns the value */
    OWNER_MOVED,        /* Value was moved away */
    OWNER_BORROWED,     /* Value is borrowed (immutable) */
    OWNER_MUT_BORROWED, /* Value is mutably borrowed */
    OWNER_DROPPED       /* Value was dropped */
} OwnershipState;
Enter fullscreen mode Exit fullscreen mode

Each variable tracks its ownership state, active borrows, and move history. When you write this Rust:

let mut x = 5;
let y = &x;       // immutable borrow
let z = &mut x;   // ERROR: cannot borrow as mutable
println!("{}", y);
Enter fullscreen mode Exit fullscreen mode

The compiler catches it and produces the same error message you would expect from rustc:

error[E0]: cannot borrow `x` as mutable because it is also borrowed as immutable
  --> source.rs:3
  = help: try using the immutable borrow after the mutable borrow ends
Enter fullscreen mode Exit fullscreen mode

Non-Lexical Lifetimes

The checker implements NLL -- borrows end at their last use, not at the end of the lexical scope. This is what modern Rust does (stabilized in the 2018 edition), and it means more code compiles correctly.

void analyze_nll(Variable* var) {
    for (int i = 0; i < var->borrow_count; i++) {
        Borrow* b = var->borrows[i];

        if (b->is_active && b->line_last_used < current_line - 1) {
            /* Borrow could end early */
            printf("    ; NLL: borrow of %s could end at line %d\n",
                   var->name, b->line_last_used);

            /* Automatically end the borrow */
            b->is_active = 0;
            if (b->is_mutable) {
                var->active_mut_borrow = -1;
            } else {
                var->active_immut_count--;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After NLL analysis, a mutable borrow that previously would have been rejected can succeed because the conflicting immutable borrow ended at its last use, not at scope exit. This is not a simplified model -- it is how rustc actually works, implemented in 500 lines of C.

AltiVec SIMD: 4x Throughput for Free

The G4 and G5 have AltiVec (aka Velocity Engine) -- 128-bit SIMD that processes 4 floats or 16 bytes per cycle. The compiler generates AltiVec instructions for operations that benefit from vectorization.

Float operations go wide automatically:

void emit_altivec_float_ops(const char* op, int count) {
    if (strcmp(op, "mul") == 0) {
        printf("    lvx v1, 0, r3     ; Load a\n");
        printf("    lvx v2, 0, r4     ; Load b\n");
        printf("    vmaddfp v3, v1, v2, v0 ; Multiply-add\n");
        printf("    stvx v3, 0, r5    ; Store result\n");
    }
}
Enter fullscreen mode Exit fullscreen mode

Arc uses the PowerPC atomic instructions (lwarx/stwcx.) for lock-free reference counting -- the same instructions that modern ARM's ldxr/stxr were modeled after:

// Arc decrement with retry loop
printf("    lwarx r4, 0, r3   ; Load reserved\n");
printf("    subi r4, r4, 1    ; Decrement\n");
printf("    stwcx. r4, 0, r3  ; Store conditional\n");
printf("    bne- .-12         ; Retry if failed\n");
Enter fullscreen mode Exit fullscreen mode

The codegen also covers CSS color blending (4 RGBA channels in one vector operation), string processing (16 bytes at a time), and iterator maps (process 4 elements per cycle). All of this matters for the Firefox goal.

Async/Await on Tiger: select() Is All You Need

Tiger has no epoll. No io_uring. It has kqueue, but we targeted the lowest common denominator: select() -- the original Unix multiplexing call from 4.2BSD in 1983. It works on every Unix Tiger can talk to, and the implementation is trivially portable.

The async runtime (rustc_async_await.c, 900+ lines) transforms async fn into state machines:

async fn fetch_data() -> String {
    let response = http_get(url).await;
    let parsed = parse_json(response).await;
    parsed.data
}
Enter fullscreen mode Exit fullscreen mode

This becomes an enum with states for each .await suspension point:

typedef enum {
    STATE_START,
    STATE_AWAIT1,    // Waiting on http_get
    STATE_AWAIT2,    // Waiting on parse_json
    STATE_COMPLETE,
    STATE_POISONED   // Panicked during poll
} AsyncState;
Enter fullscreen mode Exit fullscreen mode

The state machine stores local variables that need to survive across await points:

typedef struct {
    AsyncState state;
    int await_index;
    Future* pending_future;
    LocalVar locals[MAX_LOCALS];
    int local_count;
    void* result;
    size_t result_size;
} AsyncStateMachine;
Enter fullscreen mode Exit fullscreen mode

The I/O layer generates raw PowerPC assembly that calls select() with zero-timeout for non-blocking polls:

_async_io_poll:
    ; r3 = fd, r4 = for_read
    mflr r0
    stw r0, 8(r1)
    stwu r1, -160(r1)     ; fd_set is 128 bytes on Tiger

    ; Clear fd_set
    addi r5, r1, 32
    li r6, 32
    li r7, 0
.L_clear_fdset:
    stw r7, 0(r5)
    addi r5, r5, 4
    bdnz .L_clear_fdset

    ; FD_SET(fd, &fdset)
    srwi r5, r3, 5        ; fd / 32
    slwi r5, r5, 2        ; * 4
    addi r6, r1, 32
    add r5, r5, r6
    andi. r6, r3, 31      ; fd % 32
    li r7, 1
    slw r7, r7, r6
    lwz r8, 0(r5)
    or r8, r8, r7
    stw r8, 0(r5)
Enter fullscreen mode Exit fullscreen mode

It is not elegant. It is correct. select() has been in every Unix since 1983 and it will be in every Unix long after io_uring is deprecated. We also implement the Future trait, Pin<T>, Waker/Context, a single-threaded executor, and join!/select! combinators. Enough for Firefox's async networking.

The Tiger Toolkit: Modern HTTPS on a 2005 Mac

A Rust compiler is useless if the machine cannot download dependencies. Tiger's OpenSSL is ancient and its Python has no SSL support. So we built a complete modern networking stack using mbedTLS 2.28 LTS as the crypto foundation:

Tool What It Gives You
wget HTTPS downloads with TLS 1.2 + ChaCha20-Poly1305
curl HTTP library for git HTTPS and API calls
OpenSSH 9.6 Replaces Tiger's CVE-riddled OpenSSH 4.5
rsync 3.2 Secure file sync with xxHash checksums
PocketFox Native Cocoa browser with embedded TLS

Every tool links against mbedTLS compiled for PowerPC. No system OpenSSL dependency. The build order matters -- mbedTLS first, then wget (so you can download everything else), then OpenSSH, curl, and rsync.

# Build mbedTLS for PowerPC
gcc -arch ppc -std=c99 -O2 -mcpu=7450 -I../include \
    -DMBEDTLS_NO_PLATFORM_ENTROPY -c *.c

# Build wget with modern HTTPS
gcc -arch ppc -std=c99 -O2 -DHAVE_MBEDTLS \
    -I./mbedtls-2.28.8/include -o wget \
    wget_tiger.c pocketfox_ssl_tiger.c \
    -L./mbedtls-2.28.8/library -lmbedtls -lmbedx509 -lmbedcrypto
Enter fullscreen mode Exit fullscreen mode

Once wget works, the machine can participate in the modern internet again.

PocketFox: The Endgame Is Firefox

The whole point of building a Rust compiler for PowerPC is to compile Firefox. Modern Firefox is written in Rust and C++. TenFourFox (the last PowerPC Firefox fork) was abandoned in 2021 and is stuck at a pre-Rust version.

PocketFox is our approach: a minimal Firefox with built-in mbedTLS, bypassing Tiger's broken SSL entirely. The architecture is clean:

+---------------------------------------------+
|           PocketFox Browser                  |
+---------------------------------------------+
|  PocketFox SSL Bridge (pocketfox_ssl.h)      |
+---------------------------------------------+
|         mbedTLS 2.28 LTS                     |
|    (Portable C, no system deps)              |
+---------------------------------------------+
|     PowerPC Mac OS X Tiger/Leopard           |
+---------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

The Rust compiler patches Firefox's Rust components for PowerPC: WebRender gets AltiVec instead of SSE, encoding_rs gets big-endian fixes, parking_lot gets PowerPC atomics. Build time is 8-12 hours on a dual G4. Four hours on a quad G5.

We are not there yet, but all the compiler features are ready.

Why Bother? RustChain and Digital Preservation

Two practical reasons beyond "because it's there."

RustChain mining. Our Proof-of-Antiquity blockchain gives G4 Macs a 2.5x mining multiplier over modern hardware. Running Rust natively means we can write miners, wallets, and node software in Rust instead of Python. Real systems programming on real vintage silicon.

Digital preservation. Millions of documents, projects, and creative works were made on PowerPC Macs. When those machines cannot connect to the internet, cannot run modern software, and cannot transfer files securely -- that work becomes inaccessible. A working Rust compiler and modern TLS stack keeps these machines functional.

There is also a third reason: it is a genuinely interesting compiler engineering problem. Mapping Rust's ownership model to PowerPC register conventions, implementing NLL in 500 lines of C, generating AltiVec SIMD from generic Rust types -- this is the kind of work that makes you understand both languages deeply.

Try It Yourself

The full source is on GitHub:

github.com/Scottcjn/rust-ppc-tiger

If you have a PowerPC Mac:

# Clone the repo
git clone https://github.com/Scottcjn/rust-ppc-tiger.git
cd rust-ppc-tiger

# Build the compiler
gcc -O3 -mcpu=7450 -maltivec -o rustc_ppc rustc_100_percent.c

# Compile your Rust code
./rustc_ppc your_program.rs -o your_program.s
as -o your_program.o your_program.s
gcc -o your_program your_program.o

# Test the borrow checker
gcc -o borrow_test rustc_borrow_checker.c
./borrow_test --demo
Enter fullscreen mode Exit fullscreen mode

If you do not have a PowerPC Mac, star the repo anyway. A year of real hardware work, electricity bills, and a dedicated lab went into this.

Related projects:

More from this series:


BoTTube Videos

Built by Elyan Labs in Louisiana. Designed by Scott, coded with Claude.

Top comments (0)