Compiler quirks¶
hcc is beta software, and its README says as much. The bugs below were each hit while writing this guide or Ossuary, and each was reproduced in a small test program against hcc 0.0.15-beta (commit 06ca611), as a normal build and under -jit, on Linux x86_64 unless the entry says otherwise. They were last re-tested in October 2026, when 06ca611 was still the newest commit upstream, and each bug links to its upstream issue. If you're on a newer hcc, test before relying on either the bug or the workaround.
The dangerous ones compile without any warning and give wrong answers, so they're listed first.
Negating an indexed value¶
When the operand of unary minus is an array or pointer index, hcc 0.0.15 negates the entire rest of the expression, not just the operand. This happens with integers too. Plain variables, struct members (-p->x), function calls and dereferences (-*p) are fine.
Workaround: reorder so the negated term isn't first, or use parentheses.
Upstream: #283.
Float comparisons¶
Two related bugs:
Comparing a float with an integer literal truncates the float and compares as unsigned, so negative values look huge. Always write float literals (0.0, 1.0).
A float comparison that's assigned or passed as an argument evaluates to 0. It works as the condition of if, while or for, and as a return value.
Workaround:
Pointers and wide integers in && and ||¶
U8 *a = MAlloc(16), *b = MAlloc(16);
if (a && b) // FALSE whenever b's address ends in 0x00
Use(a, b);
return tiles && name; // same, and the result isn't 0 or 1 either
&& and || test their left operand correctly, but only look at the low byte of the right operand. A pointer or I64 on the right whose low byte is zero counts as false: 0 || 0x100 is false, and so is 5 && 0x200. MAlloc returns 8-byte aligned addresses, so a && b on two valid pointers fails about one time in 32. The value of a && b isn't 1 either: it's the right operand's low byte, sign-extended, so 5 && 0x2A1 is -95.
Comparisons, !x, Bool and other one-byte values are all safe on the right, since they're already 0 or 1.
Workaround: compare explicitly, or test with !.
Upstream: #286.
Unsigned 64-bit shifts and conversions¶
>> on a U64 copies the sign bit like a signed shift, and converting a U64 to F64 treats it as signed. Both only show up when the top bit is set, which is exactly what the output of a random number generator or a hash often looks like. U32 and smaller types are fine.
Workaround: mask off the bits the shift may have filled.
Upstream: #287.
%d in a print statement is 32-bit¶
I64 big = 1 << 40;
"%d\n", big; // prints 0
"%ld\n", big; // prints 1099511627776
StrPrint(buf, "%d", big); // "1099511627776"
A bare string used as a print statement formats like C's printf: %d, %x and %u take only the low 32 bits of an I64, so 2^31 prints as -2147483648 and 2^40 as 0. The value itself is fine. StrPrint and MStrPrint use TempleOS's formatter instead, where %d is the full 64 bits and %ld is an error ("Invalid format char").
Workaround: in print statements, use %ld, %lx and %lu for anything that can pass 32 bits. In StrPrint and MStrPrint, keep %d.
Upstream: #117.
Bool && float comparison¶
hcc prints WARNING: Binop has returned null type, Binary type is being assigned as NULL and error: The `&&` operator cannot be applied to a character type with a binary op of float type, then exits with status 1 without writing a binary.
Workaround: nest the conditions.
Upstream: #288.
Small classes in and out by value under -jit¶
class Cell { I32 x, y, z; };
Cell Add(Cell a, Cell b); // right in a normal build, garbage under -jit
Under hcc -jit, a function that takes a class by value and returns one gives wrong values when the class is made of integers and is 16 bytes or smaller (3 x I32, 2 x I64, 3 x U8 and so on). Normal builds get every case right. Under -jit, passing such a class in or returning one is fine on its own, and so are classes of floats and classes bigger than 16 bytes.
Workaround: pass an output pointer, which is how all the maths in this guide is written.
Upstream: #289.
Call results into a parameter's fields¶
Assigning the result of a call straight into a field of a pointer parameter crashes when the field isn't the first one in the class and the call takes arguments. hcc keeps the parameter in the register it arrived in, the call's arguments overwrite that register, and the store goes to a garbage address. It happens under -jit too.
Which register matters: a pointer that arrived second only breaks when the call has at least two arguments. Copying the parameter into a local (Game *h = g; h->cam_y = MaxI64(...)) doesn't help, because the local stays in the same register. These are fine: the first field (offset 0), calls with no arguments, and pointers that didn't come from a parameter, like globals or &g_game.
Workaround: put the result in a local first.
Upstream: #290.
Initialised globals and Main's arguments¶
U64 g_seed = 1;
I32 Main(I32 argc, U8 **argv)
{
"%d\n", argc; // 1 on Apple Silicon, whatever arguments were passed
return 0;
}
On AArch64 (Apple Silicon), a global with an initialiser that hcc can't place in the data section, such as I64 g_count = 0; or U64 g_seed = 1;, makes Main's argc and argv wrong. hcc runs those initialisers in a small main before calling your Main, and it uses the registers holding argc and argv to do it. argc ends up as the last value stored and argv as the global's address, so a program that checks for a flag never sees it, and one that reads argv[1] can crash. x86_64 isn't affected. Array initialisers and string pointers (U32 g_palette[16] = {...}, U8 *g_name = "...") don't trigger it.
Workaround: don't initialise scalar globals where they're declared. Globals start at zero, so = 0 can just be dropped, and any other starting value can be set at the top of Main or in a setup function it calls.
Upstream: #291.
One global per declaration¶
This applies to globals only. Locals and class fields can share a line.
Workaround: one global per line.
Upstream: #292.
Function-like macros crash the compiler¶
Workaround: write a function. Plain #define NAME value works fine.
Upstream: #293.
Address of an extern function¶
Taking &SomeCFunction of an extern "c" function fails to link as PIE (it works under -jit). Call C functions directly instead of storing pointers to them.
Upstream: #294.
Defined before use¶
A function used before its definition fails with "Variable or function has not been defined". Either order the file so definitions come first, or add a prototype (I64 Later(I64 a);). This looks like a design choice rather than a bug, so it isn't reported upstream.
No ternary¶
a ? b : c is a syntax error. Use if/else.
Upstream: #206.
How to report a new one¶
- Cut it down to the smallest program that shows the problem. Ten lines is typical.
- Print the expected and actual values side by side.
- Check whether it happens with
hcc -jitas well as a normal build. - Open an issue at Jamesbarford/holyc-lang with the program and
hcc --version.
Then add it here with a workaround.