Code Compare
The same task, eight ways
Pick a topic to see it written in C, C++, HolyC, Zig, Hare, Odin, and Forth, side by side, with a note on what is idiomatic in each.
Manual Memory: allocate & free
Every language here is garbage-collector-free, so a heap allocation is a resource you own and must hand back. The task is the same in all seven: put a single int on the heap, set it to 42, print it, then release it. Watch who frees the memory - by hand (free/Free/FREE), via a destructor (C++ RAII), or scheduled with defer (Zig, Hare, Odin) - and notice that every malloc needs exactly one matching free: no more (double-free), no fewer (leak).
Pointers & Addresses
A pointer is a value that holds the address of another value, letting you read and write that storage indirectly. This topic takes the address of a variable, mutates it through the pointer, and prints both the value and (conceptually) its address - the foundation every systems language builds allocation, aliasing, and ownership on top of. Watch where the storage lives: most of these examples point at a stack variable (no cleanup), and the heap variants make allocation and free explicit so the lifetime is obvious.
Dynamic / Growable Arrays
A growable array is a heap buffer that resizes as you append. The hard part is memory: who owns the buffer, how it grows (usually by reallocating into a larger block and copying), and who frees it. Here every language builds the same list [1,2,3], appends a 4, prints it, and cleans up - so you can see exactly where the allocation, the capacity-vs-length split, and the free live in each.
Ownership & Lifetimes
How each systems language answers the central memory question: who owns this allocation, and who is responsible for freeing it? We write one task in all seven languages - a function that allocates and returns owned memory, and a caller that must release it - to compare ownership encoded in comments (C, HolyC), in the type system (C++ unique_ptr moves), and in convention plus defer (Zig allocators, Hare, Odin), versus Forth's heap-less, region-based model.
Resource Cleanup: defer vs RAII vs manual
The same job in seven languages: acquire a resource (allocate a buffer, then open a file), use it, and guarantee the buffer is freed and the file closed on every exit path - including early return and error. This is the core memory-management question: who owns the resource, and what runs the cleanup? Compare C's goto cleanup ladder, C++ RAII (destructors that run automatically as scopes unwind), Zig's defer/errdefer, the defer of Hare and Odin, and the fully manual paths in HolyC and Forth.
Custom Allocators & Arenas
The default heap allocator (malloc/free, new/delete) is general-purpose but slow and easy to leak: every object is tracked, freed, and reclaimed individually. Arena (a.k.a. region or bump) allocators flip the model - you carve allocations off a contiguous block by simply advancing a pointer, never free anything individually, and then reclaim the entire region in one operation. This makes per-allocation cost nearly free, eliminates whole classes of use-after-free and leak bugs for same-lifetime data, and is why explicit, swappable allocators are a first-class concern across systems languages.
Memory Layout: stack, heap & structs
A struct's bytes aren't laid out the way you wrote them: the compiler inserts padding so each field lands on its natural alignment, so sizeof is usually bigger than the sum of the fields. This topic builds the same struct in every language, places one instance on the stack (automatic, freed when its scope ends) and one on the heap (you own it, you free it), and prints sizeof / alignment / field offsets. Watch three things: the padding holes inside the struct, the lifetime difference between the stack copy and the heap copy, and who is responsible for releasing the heap block.
Strings & Their Ownership
A string is just bytes plus a way to know where it ends - and that choice drives the memory model. C uses a bare char* that is NUL-terminated: the length is implicit (you scan for the \0), and you own every byte by hand. C++ std::string owns a heap buffer and frees it via RAII. Zig, Hare, and Odin use slices ([]u8) that carry an explicit length alongside the pointer, so there is no terminator to forget - but a heap-allocated slice is still a resource someone must free. We build the same greeting "Hello, C!" in all seven, concatenating "Hello, " with a name, printing it, and releasing any heap it took.
Hello, World
The classic first program, recast for seven GC-free systems languages: print the single line Hello, systems!. Even this trivial task exposes each language's personality - what the entry point looks like (or whether one is needed), how a string literal lives in static read-only memory (no allocation, nothing to free), and how output reaches the terminal. Watch that none of these need the heap: the text is baked into the binary, so this is the rare topic with no malloc/free in sight - the perfect baseline before the memory-heavy topics that follow.
Variables & Types
Before any heap appears, you need values: an integer, a float, a boolean, and a fixed-width integer like u8 or i64. This topic declares all four and prints a one-line summary in each language, so you can read the type spellings side by side. The thing to watch is signedness and width: systems code lives and dies on whether an integer is 8 or 64 bits and whether it can go negative, and these languages differ on which defaults are explicit (Zig, Hare, Odin name the width up front) versus implementation-defined (C/C++ int is at least 16 bits; char may even be signed or unsigned).
Functions: declaration, definition & argument passing
A function bundles up code behind a name, takes typed parameters, and returns a result. The task is identical across all seven: define max(a, b) returning the larger int, then call it on 7 and 3. Watch how arguments travel: integers here are passed by value (copied onto the callee's stack frame), so the function never touches the caller's storage and there is nothing to allocate or free - the contrast with by-reference/pointer passing (where the callee can mutate or must not outlive the caller's data) is exactly where memory bugs begin.
Control Flow: loops & conditionals
The bread and butter of every imperative language: a for loop and an if. The task is identical in all seven - sum the integers 1..5, but use an if to skip the even numbers, then print the total of the odds (1 + 3 + 5 = 9). Because the accumulator is a single integer living in a register or on the stack, this topic touches no heap at all - no malloc, nothing to free - so it's a clean look at each language's loop and branch syntax before the memory-heavy topics. Note how the C-family share for (init; cond; step) while Zig, Hare, Odin, and Forth each spell iteration their own way.
Structs & Records
A struct (or record) groups named fields into one composite value, laid out as a contiguous block of memory. The fields sit side by side at fixed offsets (subject to alignment padding), so a Point{x, y} is just two ints back to back - no header, no hidden pointers. The task is the same in all seven languages: declare a Point{x, y: int}, build one, translate it by (3, 4), and print the result. Watch where the struct lives - by value on the stack (the common case), or boxed on the heap when you allocate and free it explicitly.
Enums & Tagged Unions
A tagged union (also called a sum type, variant, or discriminated union) stores one of several shapes at a time plus a tag saying which - the bytes of every arm overlay the same storage, so the whole value is only as big as its largest member. The task is identical in all seven: model a Shape that is either a Circle(radius) or a Rect(w, h), then compute its area. Watch the safety spectrum, from C/HolyC's hand-rolled enum + union where you guarantee the tag matches the live arm, to C++ std::variant and Zig/Hare/Odin's built-in tagged unions where the compiler enforces exhaustive matching - while Forth, having no types at all, just lays out cells by hand.
Error Handling: codes, exceptions & error unions
One task, seven philosophies of failure. Each language parses a string into an integer and computes 100 / n, and each must handle two distinct failures: the string isn't a number, and the number is zero (divide-by-zero). Watch how the answer differs: C threads a status through return values and errno; C++ throws exceptions that unwind the stack; Zig uses error unions (!T) with try; Hare uses tagged-union error types matched with match; Odin returns multiple values and chains them with or_return; and HolyC and Forth fall back to sentinel returns and abort/CATCH. The memory angle matters here too - an error path must still free anything it allocated, so notice which schemes make that automatic and which leave it to you.
Arrays & Slices
A fixed array is a contiguous block of N elements whose size is part of its type; a slice is a lightweight view into that storage - really just a (pointer, length) pair - that owns nothing. This topic makes the same 5-int array [10,20,30,40,50], takes a view of the middle three [20,30,40], and prints them, so you can see exactly where the array lives, how a slice borrows it without copying, and how each language handles bounds.
Input & Output
Formatted output is where each language's type system meets the terminal. The task is identical in all seven - print one line, n=7 pi=3.14, mixing an integer and a float with a fixed two-decimal precision. Watch the spectrum of safety: C's printf is variadic and type-unchecked (a wrong % specifier is undefined behavior), while Zig, Hare, and Odin route everything through comptime/type-aware formatters that can't mismatch. Note too that none of this needs the heap - the format string is static read-only data and the values live in registers, so these print calls allocate nothing and have nothing to free. (std.debug.print writes to stderr; the rest go to stdout.)
Generics & Polymorphism
Generics let one piece of code work over many types instead of being copy-pasted per type. The task is the same everywhere: write a generic max (and a swap) that works for int, f64, and beyond. The systems languages split into two camps for how they specialize: monomorphization (C++ templates, Zig comptime, Odin parametric polymorphism stamp out a concrete version per type at compile time - zero runtime cost, no boxing, no allocation) versus macros/textual or weak typing (C's _Generic and macros, HolyC, and Forth's untyped stack). The memory angle: true compile-time generics keep values on the stack with no hidden indirection, whereas runtime polymorphism (void *, vtables) would add pointers, boxing, and lifetimes to manage.
Modules & Compilation
Real programs span many files, and how they split is where each language's compilation model shows: C/HolyC paste headers in textually, C++20 has true named modules, while Zig (@import a file), Hare (a directory of .ha files), and Odin (a directory = a package) treat files and folders as first-class units; Forth just INCLUDEs another file into one global dictionary. The task is identical in all seven - define a Circle type plus a factory and an area function in one unit, then use it from a second file - and the memory lesson is the same: when a factory allocates on the heap, the alloc/free pairing crosses the module boundary, so watch who owns the object and who must release it.
Interfaces & Dynamic Dispatch
Runtime polymorphism means picking which area() to run only when the program runs, based on the object's concrete type. Most systems languages have no built-in interface or class hierarchy for this - you build it yourself out of plain data, either with a vtable (a struct of function pointers the object carries) or a tagged union (one value that knows its variant and is switched on). The same task in all seven languages - a Shape whose area() dispatches at runtime - lays the two approaches side by side, and the memory angle is central: a vtable is one shared, static table pointed at by every instance, so the per-object cost is a single pointer, while the objects themselves still need explicit allocation and freeing.
Concurrency & Threads
The same job across all seven systems languages: spawn a thread (or two), have it compute a sum, then join and read the result. With no garbage collector and a shared address space, the real question is how data crosses the thread boundary - almost always as an explicit pointer into memory that outlives the spawn, freed only after join. Compare C pthreads, C++ std::thread (RAII + join-or-terminate), Zig std.Thread (no hidden allocation), Odin core:thread, and Hare's bare-bones clone(2) (its stdlib still ships no thread module), then note that HolyC is cooperatively multitasked (Spawn/Yield, no preemption) and Forth has no threads at all without an extension.
Metaprogramming & Compile-Time Code
Metaprogramming is code that runs (or generates code) before your program does, moving work from run time to compile time. The task is the same in every language: build a lookup table of squares at compile time so it costs zero work and zero allocation at run time, plus show each language's generic/code-gen idiom. The split is sharp: textual macros (C's preprocessor, HolyC's #define) blindly substitute tokens; real compile-time execution (C++ constexpr/templates, Zig comptime) runs the same language at build time with full type checking; homoiconic metaprogramming (Forth's immediate words) lets the program extend its own compiler. Memory angle: a comptime/constexpr table lives in read-only static storage (.rodata) -- no malloc, no initializer loop, nothing to free.
Bit Manipulation
Bit twiddling is the heart of systems programming: flags packed into a single word, hardware registers, bitsets. The task is identical in all seven languages - start from a value, set a bit with | (1 << n), clear it with & ~(1 << n), test it with (x >> n) & 1, then print the result in binary and hex. The integer lives in a register or on the stack, so this topic touches no heap - it's a pure look at each language's bitwise operators (&, |, ^, ~, <<, >>) and the helpers some of them add: C++'s std::bitset, Zig's @shlExact/std.math, Odin's bit_set, and Forth's hand-rolled masks.
C Interop / FFI
The reason all seven languages live in the same family is the C ABI: a stable, OS-level contract for how functions pass arguments, return values, and lay out structs. Because every one of them can speak it, they can all call straight into libc (and any C library) with no marshalling, no runtime bridge, and no copies. The task is identical everywhere - call C's puts, plus a libc function that allocates (strdup) - and the deep lesson is memory ownership across the boundary: a pointer that crosses the FFI line carries no lifetime with it, so when C mallocs the result of strdup, you must call C's free on it, never your own language's allocator.