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.
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:
- Ring-0-only. There is no user mode. Every line of code - the compiler, the editor, the games, your scratch script at the prompt - runs at the highest privilege level, with direct access to every instruction and every byte of RAM. There is no syscall boundary because there is nothing on the other side of it.
- Identity-mapped. Virtual addresses equal physical addresses. Long mode on x86-64 requires paging to be on, so Davis turned it on and then made it a no-op: the page tables map every address to itself, using large (2 MiB or 1 GiB) pages to keep them small. Nothing swaps to disk. A pointer is a real physical address, and you can print it and go look at the chip.
- No memory protection. Because there is one address space and one ring, there is no per-process isolation and no fault on a stray write. A bug that would earn a segfault on Linux instead quietly corrupts whatever it landed on. The bargain - simplicity and total transparency in exchange for safety - is made openly and on purpose.
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:
I64is the default integer, and all values are widened to 64 bits when accessed - intermediate arithmetic is done in 64-bit registers. On a machine that is purely 64-bit and identity-mapped, the native word is the type, so HolyC simply makes the native word the default rather than carrying C's historicalint/long/long longtower.F64is the only floating-point type. There is noF32. One float, the one the hardware does best.
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:
- One address space, one ring, no protection - so a pointer is a real physical address, the shell and compiler and kernel share memory directly, and the cost of that intimacy is that nothing stops a bad write.
- The task as the unit of ownership - so cleanup is lifetime-scoped (memory dies with its task) rather than per-allocation, with the immortal Adam task standing in for kernel memory.
- A JIT that is also the shell - so there is no boundary between typing a command and compiling a program; the language is the interface to the machine.
- A type system trimmed to the hardware -
I64because the machine is 64-bit, oneF64because that is the float that matters, andU0as an honest zero-sized "nothing."
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
- a complete, internally consistent computer where you can hold the whole memory
model in your head, from the identity-mapped page tables up through
MAllocto a string literal that prints itself. Terry Davis built that alone, while contending with a serious illness, and he gave it away freely. The respectful thing - and the technically correct thing - is to take the engineering seriously. On a memory-focused reading, it earns it.
Sources: TempleOS - Wikipedia · HolyC reference (TempleOS docs) · Memory Overview (TempleOS docs) · A Language Design Analysis of HolyC - Harrison Totty