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

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

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

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:

  • MAlloc effectively 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's free(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

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

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.