← History

HolyC and TempleOS

A respectful technical look at Terry Davis's HolyC - JIT-compiled at the prompt, the language used as the shell, U0 and I64, and a single ring-0 address space where every MAlloc is unprotected and every byte is yours.

HolyC

Most of the languages on this site exist inside an operating system someone else wrote. They allocate through a libc that allocates through mmap/brk, which asks a kernel running in a different privilege ring, in a different address space, behind a page-table boundary that will kill your process the instant a pointer wanders out of bounds. The memory model you actually program against is a negotiation with that kernel.

HolyC is different because it has no kernel to negotiate with. It is the kernel. Terry A. Davis wrote both the language and the operating system it lives in - TempleOS - as a single, coherent, deeply personal artifact, alone, over more than a decade. He described it as a temple: a 64-bit, ring-0, single-address-space machine deliberately built to the proportions of a 1980s home computer, where a person could understand the whole thing. He lived with severe schizophrenia, and he built this in spite of it. The result is technically unusual in ways worth studying on the merits, and that is how this article treats it: accurately, and with respect for the person and the work.

The thing to hold onto, for a memory-focused site, is that HolyC's design and TempleOS's memory model are the same decision viewed from two angles. You cannot explain U0, the parentheses-optional call, or the JIT-at-the-prompt without explaining the flat, unprotected, identity-mapped address space they run in. So this article alternates between the two.

The substrate: one ring, one address space, no protection

TempleOS is an x86-64, multi-core, non-preemptively multitasking, ring-0-only, identity-mapped operating system. Each of those words is a load-bearing memory decision:

One more constraint shapes everything: the compiler emits 32-bit signed-relative JMP and CALL instructions, because a full 64-bit call takes two instructions and Davis valued the smaller, faster one. A signed 32-bit displacement only reaches ±2 GiB, so all code must live in the low 2 GiB of memory - the "code heap." That is not a footnote; it is why allocation in TempleOS is split into a code pool and a data pool, and it is a clean example of an instruction-encoding choice dictating a memory-layout policy.

The language at the prompt: HolyC is the shell

In a normal OS the shell and the compiler are separate programs that speak to each other through files and processes. In TempleOS there is no such separation, because every program except the initial kernel/compiler is JIT compiled on demand, and the command line is simply a HolyC interpreter that compiles and runs each line as you type it. The compiler is fast enough to make this feel interactive - it can churn through tens of thousands of lines in well under a second.

So the "shell" is the language. You don't have a Dir command that the shell parses; you have a HolyC function named Dir, and you call it. And because HolyC lets you call a function that takes no arguments (or only defaulted ones) without parentheses, the call looks exactly like a shell command:

// All three of these are the same call, typed at the TempleOS prompt.
Dir("*");   // explicit argument
Dir();      // empty argument list
Dir;        // no parentheses at all - reads like a shell command

That single grammar rule is what collapses "command" and "function call" into one concept. There is no quoting layer, no argv string-splitting convention, no separate scripting language: the thing you type interactively and the thing you write in a .HC source file are the same HolyC, compiled by the same JIT, running in the same ring-0 address space. A "script" is just an #include from the command line.

The same minimalism shows up in output. A string constant on its own is a complete statement - it is sent to Print() - so the canonical first program is not a function call, it is a value:

"Hello World!\n";

There is no #include <stdio.h>, no main, no printf. The literal is the program. Compare the ceremony in standard C:

#include <stdio.h>
int main(void) {
    printf("Hello World!\n");
    return 0;
}

This is the whole personality of HolyC in one line: it is C with the ritual removed, fused to a machine where there is nothing between your code and the hardware.

U0 and I64: the type system, and why it is shaped this way

HolyC's numeric types are spelled by signedness, kind, and bit width: I8/U8, I16/U16, I32/U32, I64/U64, and F64. I is signed integer, U is unsigned, F is float; the number is the bit count. Two choices stand out:

Then there is U0 - the type that everyone remembers. U0 is "void, but zero size." It is the return type of a function that yields no value (so it behaves like C's void in that role), but unlike C's void it is a genuine zero-width type rather than a special incomplete type. Functions with no declared return type default to U0:

U0 Greet(U8 *name)
{           // U0: returns nothing, and that "nothing" has size 0
  "Hi, %s!\n", name;   // a format string + args is sent to Print()
}

Greet("Terry");

A few more deliberate differences from C, each of which removes a layer rather than adding one:

// Ranged comparisons: write the math, not the boolean spelling of it.
if (13 <= age < 20)
  "Teen-ager\n";          // means 13 <= age && age < 20

// Switch with case ranges (HolyC also supports auto-incrementing case
// values, e.g. a bare `case:`, not shown here).
switch (grade) {
  case 90...100: "A\n"; break;
  case 80...89:  "B\n"; break;
  default:       "see me\n";
}

// Default arguments may sit anywhere; a leading comma skips to a later one.
I64 Box(I64 w = 8, I64 h)
{
  return w * h;
}
Box(, 6);   // w defaults to 8, h = 6  ->  48

HolyC also drops pieces of C that Davis considered clutter or footguns: there is no #define macro system, no typedef (you use class, which also serves as struct), #include takes "quotes" not <angles>, and there is no continue keyword - he steered programmers toward goto. It is C with the grammar opinionated toward one programmer's taste, not a committee's.

Memory management: MAlloc, Free, and task-owned heaps

Here is where the language and the OS meet most directly. HolyC's allocator looks like C's at the surface - MAlloc/Free in place of malloc/free - but the semantics are bent to fit the single-address-space, per-task design.

// Allocate, use, free. No header to include - the language is the system.
U8 *buf = MAlloc(4096);          // bytes from the current task's heap
// ... fill buf ...
Free(buf);                        // release it
Free(NULL);                       // explicitly legal: freeing NULL is a no-op

Three properties are worth pulling out, because each is a real design choice:

1. Allocation is per task, and a task's memory dies with it. Every task has its own heap. Memory a task allocated is automatically freed when that task is killed - a coarse, lifetime-scoped cleanup that means a crashed or closed program does not leak across the system, even with no protection boundary. This is closer to an arena-per-process model than to a global C heap: the task is the arena, and ending the task resets it.

2. The Adam task's heap is the "kernel" memory. TempleOS has a root task called Adam that never dies. Because task memory is freed only when the task ends, anything you allocate on Adam's heap lives for the life of the machine - it is the equivalent of kernel/global memory in a conventional OS. There is even a separate family of Adam allocators (AMAlloc, ACAlloc, AMAllocIdent, …) for explicitly putting allocations on that permanent heap.

3. You can build private heaps, and allocate off any heap. A heap in TempleOS is a CHeapCtrl. You can spin up an independent one with HeapCtrlInit() and then direct allocations into it by passing it as the second argument to MAlloc - so you can MAlloc/Free against another task's heap, or against a private pool you own:

CHeapCtrl *hc = HeapCtrlInit(NULL, Fs);   // an independent heap
U8 *p = MAlloc(256, hc);                   // allocate from *that* heap
Free(p);                                    // returns to hc

And because larger requests are rounded up to a power of two, MSize tells you the real size you got back - useful when you want to use the slack you were already charged for:

U8 *p = MAlloc(100);
I64 real = MSize(p);     // likely 128, not 100 - the rounded-up capacity

The through-line: in a system with no protection and no per-process address space, the task becomes the unit of memory ownership and the safety net. You don't get isolation; you get the guarantee that ending a task reclaims its memory.

How the neighbors solve the same problems

HolyC's choices look less eccentric when you line them up against the other systems languages on this site. Each one is answering the same questions - what's the default integer? how do I print? who owns this allocation? - with a different philosophy.

C is the obvious baseline: HolyC is a dialect of it, with the ceremony stripped and the standard library replaced by the OS.

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *buf = malloc(4096);     // from a libc heap, atop the kernel
    if (!buf) return 1;
    free(buf);                    // free(NULL) is also legal here
    return 0;
}

C++ keeps C's allocation but adds RAII so ownership is a type, and cleanup runs automatically as scopes unwind - the opposite of HolyC's "you call Free, or the task does it when it dies."

#include <memory>
int main() {
    auto buf = std::make_unique<char[]>(4096); // owns the heap block
    // ... use buf ...
    return 0;                                   // ~unique_ptr frees it here
}

Zig makes the allocator an explicit parameter - the caller chooses the strategy, and defer schedules the release next to the acquisition.

const std = @import("std");
pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();             // checks for leaks at exit
    const a = gpa.allocator();
    const buf = try a.alloc(u8, 4096);  // allocator passed in explicitly
    defer a.free(buf);                   // freed on scope exit
}

Hare keeps a libc-like alloc/free but pairs it with defer and a small, deliberate standard library - much like HolyC's minimalism, but inside a conventional protected OS.

export fn main() void = {
	let buf: *[4096]u8 = alloc([0...])!;  // heap allocation, [0...] zeroes it
	defer free(buf);                      // released at scope end
};

Odin, like Zig, threads allocators through the program - but via an implicit context.allocator you can swap per scope, and a defer for cleanup.

package main
main :: proc() {
	buf := make([]u8, 4096)   // uses context.allocator
	defer delete(buf)          // released at scope end
}

Forth sits at the far minimalist end, and here HolyC's task-heap idea has a real cousin: the native Forth model is a bump allocator over the dictionary, where ALLOT advances the pointer and reclamation is a rewind, not a per-object free.

CREATE BUF  4096 ALLOT     \ reserve 4096 bytes by bumping HERE
\ ... use BUF ...
\ reclaim by rewinding (e.g. with a MARKER), not by freeing each byte

Read those together and HolyC stops looking strange. Its "free everything when the task dies" is Forth's rewind at OS scale; its MAlloc/Free is C's heap; its parentheses-optional calls are the same impulse toward minimal syntax that drives Hare and Forth. What is genuinely singular is the fusion: one person's language, compiler, shell, and unprotected memory model, designed to fit together with nothing in between.

What made it unusual - and why it's worth studying

Strip away the legend and HolyC/TempleOS is a coherent set of memory-system decisions, each pushed to its logical end:

It is not a model anyone should ship to production: no protection, no isolation, no multi-user story, by design. But as a study object it is rare and valuable


Sources: TempleOS - Wikipedia · HolyC reference (TempleOS docs) · Memory Overview (TempleOS docs) · A Language Design Analysis of HolyC - Harrison Totty