Learn HolyC
Terry Davis's C dialect for TempleOS: JIT-compiled, interactive, with U0/I64 types and Print().
What HolyC Is: A JIT Shell, Not Just a Language
TempleOS's native C dialect doubles as the shell; source compiles and runs on the fly.
What HolyC Is: A JIT Shell, Not Just a Language
HolyC is the systems language Terry A. Davis (1969-2018) built as the beating heart of TempleOS, an operating system he wrote almost entirely by himself - kernel, editor, 64-bit JIT compiler, and well over 100,000 lines of code. The language was first called C+ and later renamed HolyC to match the project's biblical theme. Davis described it as a middle ground "between C and C++": C-like syntax with a few C++ conveniences (member functions, default arguments) but none of the heavy machinery.
What makes HolyC unusual among the seven systems languages in this collection is that the compiler is the shell. There is no separate holyc build step the way there is gcc, zig build, or cc. In TempleOS the command line is a HolyC just-in-time (JIT) compiler: when you type an expression or a function name at the prompt, it is compiled to native machine code and executed immediately.
No required main()
Because the file is a script that the JIT compiles top to bottom, top-level statements execute in order as they compile. There is no mandatory entry point.
// hello.HC -- the whole program. No #include, no main().
"Hello, TempleOS!\n"; // a bare string statement prints itself
Print("2 + 2 = %d\n", 2 + 2);
Running this file (or pasting it at the prompt) prints both lines. Compare that to C, where the same thing needs ceremony:
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
printf("2 + 2 = %d\n", 2 + 2);
return 0;
}
A bare string is a print statement
A standalone string literal at statement level is printed - this is one of HolyC's signature shortcuts and is why so much TempleOS code reads like a mix of commands and prose. The string supports printf-style format specifiers when you give it arguments:
"Plain text, printed as-is.\n";
"x is %d and y is %d\n", 3, 4; // a string statement with arguments
Files, types, and the temple
TempleOS source files use the .HC extension (and rich-text documentation lives in .DD files in the OS's own format, not plain ASCII). Everything ran in 64-bit ring 0 in a single flat address space - no memory protection, no users, no networking - by design. Davis wanted "a modern Commodore 64": a machine simple enough that one hobbyist could understand all of it.
The final official release, TempleOS 5.03, shipped on November 20, 2017. After Davis's death in 2018 the work entered the public domain, inspiring community continuations like Shrine and ZealOS and standalone HolyC compilers/transpilers that run on mainstream operating systems.
Reference
- HolyC reference (TempleOS docs mirror): https://templeos.holyc.xyz/Wb/Doc/HolyC.html
- TempleOS - Wikipedia: https://en.wikipedia.org/wiki/TempleOS
Next we meet HolyC's fixed-width type system and its zero-sized U0.
Types, U0/I64, and Print()
Fixed-width integer types, the zero-sized U0, F64, and TempleOS's Print().
Types, U0/I64, and Print()
HolyC throws out C's portable-but-vague int/long/char zoo in favor of a small, fixed-width type system. Sizes are exact and never platform-dependent, which fits an OS that only ever targets one machine: 64-bit x86 in ring 0.
The integer types
There are signed (I) and unsigned (U) integers in four widths, named by their bit count:
| Type | Bits | Signed? | C analogue |
|---|---|---|---|
I8 / U8 |
8 | signed / unsigned | int8_t / uint8_t |
I16 / U16 |
16 | signed / unsigned | int16_t / uint16_t |
I32 / U32 |
32 | signed / unsigned | int32_t / uint32_t |
I64 / U64 |
64 | signed / unsigned | int64_t / uint64_t |
I64 is the workhorse - the native register width, used for loop counters, return codes, and general arithmetic the way C code reaches for int. There is exactly one floating-point type, F64 (64-bit double); there is no 32-bit float.
I64 count = 42; // the default "integer" you reach for
U8 letter = 'A'; // a byte; chars are just U8
U64 mask = 0xFF00FF00; // unsigned 64-bit
F64 ratio = 3.14159; // the only float type
U0: a truly zero-sized void
The strangest type is U0. It plays the role C's void plays - the "no value" return type and the element type of an untyped pointer - but unlike C's void, U0 is genuinely zero bytes wide. sizeof(U0) is 0. A U0 * is HolyC's generic pointer (like C's void *).
U0 Greet() // returns nothing
{
"Hello!\n";
}
U0 *raw = MAlloc(16); // a generic, untyped pointer
Printing with Print()
TempleOS does not have C's <stdio.h>. The built-in you use constantly is Print(), which takes a printf-style format string:
I64 x = 7;
F64 pi = 3.14159;
Print("x = %d, pi = %5.2f\n", x, pi); // x = 7, pi = 3.14
Common format specifiers mirror C: %d (decimal integer), %x (hex), %c (character), %s (string), %f (float), %p (pointer). HolyC also adds quirky TempleOS-specific ones, but %d/%s/%f cover everyday use. And remember the shortcut from the last lesson - a bare string statement prints itself, so Print("hi\n") and "hi\n"; are equivalent for a constant string.
Weak, unchecked, static typing
HolyC is statically typed (every variable has a declared type) but the typing is weak and largely unchecked: implicit conversions happen freely between integer widths and pointers, with no bounds checking and no run-time type safety. This is C-level "trust the programmer" taken further - convenient at the TempleOS prompt, unforgiving when you get it wrong. Compare a strongly-typed systems language like Zig, which refuses silent narrowing:
// Zig: this is a compile error; you must convert explicitly.
const big: u64 = 300;
const small: u8 = big; // error: expected u8, found u64
const ok: u8 = @intCast(big); // explicit, and traps if it doesn't fit
HolyC would simply truncate. The discipline is on you.
Reference
- HolyC reference (types): https://templeos.holyc.xyz/Wb/Doc/HolyC.html
Next: functions, default arguments, and HolyC's parenthesis-free calls.
Functions, Default Arguments, and Control Flow
Define functions, use default and any-position arguments, and call them without parens.
Functions, Default Arguments, and Control Flow
Functions in HolyC look like C functions, with the fixed-width types from the last lesson and a handful of C++-flavored conveniences sprinkled on top.
Defining and returning
A function declares its return type, name, and typed parameters. return works as in C; a U0 function returns nothing.
I64 Add(I64 a, I64 b)
{
return a + b;
}
U0 Banner(U8 *title)
{
Print("=== %s ===\n", title);
}
Print("%d\n", Add(2, 3)); // 5
Banner("Menu"); // === Menu ===
Default arguments, in any position
Like C++, HolyC supports default argument values - but more liberally: a default may appear on any parameter, not just trailing ones. Omitted arguments take their default.
I64 Clamp(I64 x, I64 lo = 0, I64 hi = 100)
{
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
Clamp(150); // 100 (lo, hi default)
Clamp(150, 50); // 100 (hi defaults to 100)
Clamp(-5, -10, 10); // -5
Parenthesis-free calls
A distinctive HolyC shortcut: a function that needs no arguments can be invoked without parentheses, just by naming it. This is what makes the JIT shell feel like a command line - typing Dir runs the directory command.
U0 Hello()
{
"Hi there!\n";
}
Hello; // calls Hello() -- no parens needed
Hello(); // also fine
So at the TempleOS prompt, every function name is effectively a command, and that is exactly how the OS's shell works.
Control flow
Control flow is plain C: if/else, while, do/while, for, switch/case, plus break and continue.
I64 Factorial(I64 n)
{
I64 acc = 1, i;
for (i = 2; i <= n; i++)
acc *= i;
return acc;
}
I64 i;
for (i = 0; i < 5; i++)
Print("%d! = %d\n", i, Factorial(i));
switch exists and TempleOS extends it with range cases and "sub-switches", but the basic form is the familiar one:
U0 Describe(I64 n)
{
switch (n) {
case 0: "zero\n"; break;
case 1: "one\n"; break;
default: "many\n"; break;
}
}
A note on style
HolyC is whitespace-insensitive like C, and you will see TempleOS code lean on the bare-string-prints shortcut heavily, so function bodies often read as a sequence of quoted lines interleaved with logic. It is terse by design - written to be typed at a prompt as much as saved to a file.
Reference
- HolyC reference (functions, default args): https://templeos.holyc.xyz/Wb/Doc/HolyC.html
Next, the core of this collection: HolyC's fully manual, GC-free memory model.
Memory: MAlloc, Free, and Per-Task Heaps
Fully manual, GC-free memory from per-task heaps via MAlloc()/Free()/MSize().
Memory: MAlloc, Free, and Per-Task Heaps
This is the lesson that defines HolyC's place among systems languages. Memory in HolyC is entirely manual and garbage-collection-free, just like C - but the details, and the consequences, are pure TempleOS.
MAlloc and Free
You request heap memory with MAlloc() and return it with Free(). The shapes mirror C's malloc/free, but the names are capitalized and the source of the memory is special (more below).
U8 *buf = MAlloc(4096); // grab 4096 bytes from this task's heap
if (buf) { // see the note on NULL below
buf[0] = 'H';
Print("first byte: %c\n", buf[0]);
Free(buf); // return it to the heap
}
Free(NULL); // allowed: Free(NULL) is a harmless no-op
Two TempleOS-specific facts worth burning in:
MAlloceffectively never returns NULL in TempleOS. When the heap is exhausted it throws (the system aborts the task) rather than handing back a null pointer. So the C habit of checking every allocation for NULL is, strictly, unnecessary on TempleOS - though defensive code still writes it.Free(NULL)is explicitly allowed and does nothing, exactly like C'sfree(NULL).
MSize: how big is this block, really?
MSize(ptr) reports the actual allocated size of a block, which can be larger than you asked for: large requests are rounded up to a power of two. This lets you treat an allocation as a ready-made growable buffer.
U8 *p = MAlloc(1000);
Print("asked 1000, got %d\n", MSize(p)); // may print 1024
Free(p);
Per-task code and data heaps
Here is the genuinely distinctive part. Memory does not come from one global heap. Each task in TempleOS has its own code heap and data heap. MAlloc pulls from the current task's data heap by default.
The payoff: when a task dies, its heaps are reclaimed automatically. Anything you MAlloc'd and forgot to Free is recovered when the task ends - a coarse, whole-task version of the "free it all at once" idea that arenas give you elsewhere.
You can also be explicit about which heap to allocate from. MAlloc accepts an optional second argument naming a task (or heap-control structure), so one task can allocate into - or free out of - another task's heap, and you can spin up a standalone heap with HeapCtrlInit():
// Allocate against a specific task's heap (second arg), not the current one.
U8 *shared = MAlloc(256, some_task);
// ... another task can Free(shared) into the same heap ...
This per-task ownership is unique among the seven languages here. Compare the spectrum:
/* C: one global heap, you pair every malloc with a free, forever. */
char *p = malloc(256);
if (!p) return -1; /* must check: malloc CAN return NULL */
/* ... use p ... */
free(p);
// Odin: allocation routes through an implicit context.allocator.
// Swap it for an arena and one reset frees everything -- the opt-in
// version of what TempleOS gives you per task.
arena: mem.Arena
buf := make([]u8, 256) // from context.allocator
// free_all(&arena) reclaims everything at once
No memory protection: errors are unforgiving
The flip side of TempleOS's simplicity is the danger. Everything runs in 64-bit ring 0 in a single flat address space with no memory protection at all. There is no MMU sandbox between your task and the kernel. A double-Free, a wild pointer, or a buffer overrun does not segfault a single process - it can corrupt the entire system. There is no safety net.
U8 *p = MAlloc(64);
Free(p);
Free(p); // DOUBLE FREE -- in ring 0 this can corrupt the whole machine.
p[0] = 0; // USE AFTER FREE -- writing through a freed wild pointer. Same risk.
This is the same manual-memory discipline as C, hare's alloc/free, or Forth's ALLOCATE/FREE, but with the guardrails removed entirely. The trade is deliberate: maximum simplicity and control, zero protection.
Reference
- HolyC reference (MAlloc/Free/MSize, heaps): https://templeos.holyc.xyz/Wb/Doc/HolyC.html
- A Language Design Analysis of HolyC - Harrison Totty: https://harrison.totty.dev/p/a-lang-design-analysis-of-holyc
Next we put memory to work building data structures, and contrast HolyC's hand-managed cleanup with defer/RAII elsewhere.
Pointers, Classes, and Manual Cleanup
Build structures with class, walk them with pointers, and free everything by hand.
Pointers, Classes, and Manual Cleanup
With manual memory in hand, this lesson builds real data structures. HolyC aggregates data with class (its word for what C calls struct), and because there is no defer and no RAII, you free everything by hand on every exit path.
class instead of struct
HolyC uses the keyword class where C uses struct. Despite the C++-sounding name, a basic HolyC class is just a record of fields - laid out in memory exactly like a C struct. Member access uses . for values and -> through pointers, as in C.
class Point
{
I64 x;
I64 y;
};
Point p;
p.x = 3;
p.y = 4;
Print("(%d, %d)\n", p.x, p.y);
Point *pp = &p;
Print("x via pointer: %d\n", pp->x); // -> through a pointer
HolyC classes can also carry member functions and use single inheritance, leaning toward the C++ side of the family - but most TempleOS code uses them as plain data records.
Allocating structures on the heap
Combine class with MAlloc to build dynamic structures. Here is a singly linked list node and a tiny builder:
class Node
{
I64 value;
Node *next;
};
Node *NodeNew(I64 v, Node *next)
{
Node *n = MAlloc(sizeof(Node)); // one node's worth of heap
n->value = v;
n->next = next;
return n;
}
// Build 1 -> 2 -> 3
Node *head = NodeNew(1, NodeNew(2, NodeNew(3, NULL)));
Node *cur;
for (cur = head; cur; cur = cur->next)
Print("%d ", cur->value);
"\n";
Cleanup is fully manual - in reverse, on every path
HolyC has no defer, no destructors, no RAII. Whatever you MAlloc, you must Free, and you must do it on every path out of the function. The idiom is C's: acquire in order, release in reverse.
// Free a linked list: walk it, holding 'next' before freeing the node.
U0 ListFree(Node *head)
{
Node *cur = head, *next;
while (cur) {
next = cur->next; // save next BEFORE freeing cur
Free(cur); // ...otherwise this is a use-after-free
cur = next;
}
}
ListFree(head);
And a function with two resources shows the reverse-order release and the early-exit cleanup:
I64 LoadInto(U8 *path)
{
I64 rc = -1;
U8 *buf = MAlloc(4096); // resource 1
I64 fd = FOpen(path, "r"); // resource 2
if (!fd) {
Free(buf); // undo resource 1 before bailing
return rc;
}
I64 n = FBlkRead(fd, buf, 1, 4096);
Print("read %d bytes\n", n);
rc = 0;
FClose(fd); // success path: reverse order...
Free(buf); // ...close, then free
return rc;
}
Why other languages added machinery here
This hand-written ladder is exactly the pain that motivated defer and RAII in HolyC's neighbors. The same two-resource job is leak-proof "for free" elsewhere:
// C++ RAII: destructors run automatically on every exit, even exceptions.
auto buf = std::make_unique<std::vector<char>>(4096);
FilePtr f(std::fopen(path, "rb")); // custom-deleter unique_ptr closes it
if (!f) throw std::runtime_error("open"); // buf still freed by its dtor
// f closed, then buf freed, in reverse order when the scope ends.
// Zig defer/errdefer: cleanup sits next to acquisition, runs on scope exit.
const buf = try allocator.alloc(u8, 4096);
defer allocator.free(buf); // runs on every exit
const file = try std.fs.cwd().openFile(path, .{});
defer file.close(); // runs on every exit
HolyC makes the opposite trade: nothing is hidden, nothing is automatic. Every MAlloc/Free and FOpen/FClose is visible and yours to manage - and, because TempleOS has no memory protection, a single missed or doubled Free can take down the whole machine. That is HolyC: a divine little C, where you are the garbage collector.
Reference
- HolyC reference (class, pointers): https://templeos.holyc.xyz/Wb/Doc/HolyC.html
- TempleOS - Wikipedia: https://en.wikipedia.org/wiki/TempleOS
You now have HolyC end to end: the JIT shell, the type system, functions, the per-task manual memory model, and hand-managed data structures - the whole divine little language.