Language Specification Overview
This language specification will contain information about the language itself. For example, keywords, syntax of the language, as well as the unique features Occult has over other languages.
About Occult
Occult began in 2023 as a C transpiler Occult Source -> C Code -> C Compiler. This allowed for rapid development, and a working programming language pretty much eliminating the need for a custom backend, leveraging the power of C.
In 2024, the transpiler had became too buggy, and very restrictive for Occult to grow as a language, as a result, the project shifted from the C transpiler to a complete compiler rewrite which is and won't be reliant on any 3rd party backends.
Current Status
- Handles somewhat complex data structures (structs, vessels, enums, arrays, pointers, basic nesting/composition).
- Still very buggy overall, expect crashes, incomplete features, and breaking changes.
- Single-file programs only, the user may include other
*.occfiles.
Keywords
Occult has keywords split into several categories: control flow, declarations, types, and machine-code-specific keywords.
Reserved Words
if
elseif
else
loop
when
do
break
continue
while
for
true
false
return
switch
case
default
elseif can also be written as else if and it will have the same behavior.Platform Detection
Occult provides built-in boolean constants for detecting the target platform at compile time.
is_win64
is_linux64
These behave like true and false and can be used in conditions to write platform-specific code. More platform detection constants will be added as more targets are supported.
Datatypes
i64
i32
i16
i8
u64
u32
u16
u8
f32
f64
array
string
bool
char
char and bool behave the same as i8, this will most likely change in the future as more type safety is implemented.Declaration Keywords
fn
struct
vessel
enum
module
generic
const
Machine Code
shellcode
asm
shellcode allows for raw hex machine code bytes directly in a function body. asm allows for human-readable assembly mnemonics. Both are covered in the Functions section. Support for both keywords will be expanded in future releases, and more architectures will be supported over time.Misc
import
from
include
self
include pulls in another .occ file. self refers to the current vessel instance inside a method. import and from are just a nicer way to include files, ultimately functions the same as include.
Comments
Comments are the same as C.
// Single line
/*
Multi
line
comment
*/
Literals
Everything in this section will cover the types of literals Occult has.
Integral & Floating Point
There are integral and floating point literals.
Occult supports hexadecimal, binary, and octal, as well as the normal base 10 format for normal integers.
There is also support for scientific floating point literals.
Examples
// Binary
0b1010
// Hexadecimal
0xDEADBEEF
// Octal (Same notation as C++)
010 // 8
// Normal base 10
123
// Normal floating point number
3.141
// Scientific floating point
2e10
Strings & Characters
Occult has string and character literals as well.
Examples
"Hello, I'm a string literal!\n"
// Character literals
'H'
'i'
'\n'
Operators
For precedence, Occult uses a highly modified version of a shunting-yard algorithm.
| Precedence | Operator(s) | Type | Associativity | Placement |
|---|---|---|---|---|
| 2 | +x -x ~x !x |
Unary | Right | Pushed to stack |
| 2 | @x $x |
Unary ref | Right | Output / stack* |
| 3 | * / % |
Binary | Left | Stack (drained) |
| 4 | + - |
Binary | Left | Stack (drained) |
| 5 | << >> |
Bitwise | Left | Stack (drained) |
| 8 | > < >= <= |
Comparison | Left | Stack (drained) |
| 9 | == != |
Equality | Left | Stack (drained) |
| 10 | & |
Bitwise AND | Left | Stack (drained) |
| 11 | ^ |
Bitwise XOR | Left | Stack (drained) |
| 12 | | |
Bitwise OR | Left | Stack (drained) |
| 13 | && |
Logical AND | Left | Stack (drained) |
| 14 | || |
Logical OR | Left | Stack (drained) |
| 15 | = |
Assignment | Left** | Stack (drained) |
| 15 | += -= *= /= %= |
Compound | Left | Stack (drained) |
| 15 | &= |= ^= <<= >>= |
Compound | Left | Stack (drained) |
| 17 | ( ) |
Grouping | N/A | Stack / drain stop |
Reference and dereference @ and $ are special-cased among unary operators. @ (reference) is emitted directly to output rather than pushed to the stack. $ (dereference) is pushed to the stack with a count, allowing multiple consecutive dereferences e.g. $$x to be collapsed and resolved together after a closing parenthesis.
Assignment is left-associative in Occult, and chained assignment is not supported. Writing a = b = c is undefined behavior or a compile error. Assign values one at a time.
Unary !x is parsed as a prefix operator but cannot be used as a standalone condition. Write x == false instead of !x. This applies to any singular boolean expression, Occult requires explicit binary comparisons. You can still negate grouped expressions, e.g. !(x == y).
Compound Assignment Operators
Occult supports compound assignment operators as shorthand for reading and writing the same variable.
fn main() {
i64 n = 100;
n += 10; // n = n + 10
n -= 5; // n = n - 5
n *= 2; // n = n * 2
n /= 3; // n = n / 3
n %= 9; // n = n % 9
// Bitwise compound assignments
n &= 0xFF;
n |= 0x01;
n ^= 0x10;
n <<= 2;
n >>= 1;
return n;
}
++ and -- are not supported and will not be added. Use i += 1 and i -= 1 instead.Control Flow, Functions, and Expressions
Occult has many types of expressions, and several different ways to declare functions.
Declaring a Function
Declaring a function in Occult is fairly simple. The return type is optional - by default a function's return type will be i64 (64-bit integer).
Functions also implicitly have return 0 at the end if nothing is specified.
fn main() /*return type*/ { }
So, this main function is simply just returning 0 as an i64.
Alternatively, you can have a function which uses shellcode (machine code) instead of normal code, functioning similarly to inline assembly in other languages.
fn main() shellcode i64 { 0x48 0xC7 0xC0 0x0A 0x00 0x00 0x00 0xC3 }
/*
Shellcode:
mov rax, 10
ret
*/
This shellcode is just returning 10 in the main function.
ASM Blocks
You can also write functions using human-readable x86-64 assembly mnemonics with the asm keyword instead of raw hex bytes.
Syntax
fn /*name*/(/*params*/) asm /*return type*/ {
// x86-64 mnemonics
}
Example
fn print_str(string, i64) asm i64 {
push rbp
mov rbp, rsp
mov r11, rdi
mov r12, rsi
mov rax, 1
mov rdi, 1
mov rsi, r11
mov rdx, r12
syscall
mov rsp, rbp
pop rbp
mov rax, 0
ret
}
fn main() {
print_str("HELLO\n", 6);
return 0;
}
asm blocks are basic for now and will be expanded in future releases. More architectures will be supported over time.Variables
Occult supports both local and global variables. Global variables are declared at the top level, outside any function.
fn main() {
i64 v = 30;
return v;
}
This code will just return 30. Variable declarations work like they would in any other C-like language.
Constants
You can declare constants with the const keyword. Constants can be declared at global or local scope and cannot be reassigned.
const i64 MAX_SIZE = 100;
fn main() {
const i32 local_max = 42;
return local_max;
}
Calling a Function
Calling functions is the same as it would be in any other C-like language.
fn add(i64 x, i64 y) i64 {
return x + y;
}
fn main() {
i64 result = add(5, 5); // assigns 10 to result
return result; // returns 10
}
Alternatively, you can also return add directly if you want to get the same behavior.
Variadic Functions
Functions can accept a variable number of arguments using .... Inside the function, extra arguments are accessed through the __varargs built-in array.
include "../std/stdio.occ"
fn sum(i64 count, ...) i64 {
i64 total = 0;
i64 i = 0;
while i < count {
total += __varargs[i];
i += 1;
}
return total;
}
fn main() {
std::print_integer(sum(3, 10, 20, 30)); // 60
std::print_string("\n");
}
__varargs mechanism may change in future iterations of the language.Type Casting
Occult supports native casting between all integer and floating point types using a function-call syntax. Casting to or from custom types or string is not supported.
Syntax
/*target_type*/(/*expression*/)
Example
include "../std/stdio.occ"
fn main() {
i64 x = 42;
f32 as_float = f32(x); // integer to float
f32 pi = 3.14;
i64 truncated = i64(pi); // float to integer - truncates toward zero
i64 big = 300;
i8 narrow = i8(big); // narrowing cast - wraps
f64 precise = f64(pi); // widen float precision
f32 back = f32(precise); // narrow float precision
std::print_integer(truncated); // 3
std::print_string("\n");
return 0;
}
Control Flow
There are several ways for control flow in Occult.
If Statements
Syntax
if /*condition*/ { /*body*/ }
else if /*condition*/ { /*body*/ }
else { /*body*/ }
elseif is valid as well.fn main() {
i64 to_compare = 1;
if to_compare == 1 {
return 1;
}
else {
return 0;
}
}
Switch
Occult has switch statements. The expression is matched against case labels. A default case runs when no other case matches.
Syntax
switch (/*expression*/) {
case /*value*/: { /*body*/ }
default: { /*body*/ }
}
Example
include "../std/stdio.occ"
fn main() i64 {
i64 x = 2;
switch (x) {
case 1: {
std::printf("one\n");
}
case 2: {
std::printf("two\n");
}
case 3: {
std::printf("three\n");
}
default: {
std::printf("other\n");
}
}
return 0;
}
Fallthrough
Cases without a body fall through to the next case that has one, similar to C.
switch (x) {
case 1:
case 2: {
std::printf("1 or 2\n"); // runs for x == 1 or x == 2
}
default: {
std::printf("other\n");
}
}
Loop
Just like Rust, there is a loop keyword, and it behaves exactly the same.
Syntax
loop { /*body*/ }
Example
fn main() {
i64 x = 0;
loop {
x += 1;
} // Infinitely add x by 1
}
While
While loops behave as you'd expect.
Syntax
while /*condition*/ { /*body*/ }
Example
fn main() {
i64 i = 0;
while i < 5 {
i += 1;
} // Loop will run 5 times
}
For
For loops have a unique syntax, using the when keyword as well as do to make it easier for beginners to understand what's going on.
Syntax
for /*variable decl*/ when /*condition*/ do /*iterator*/ { /*body*/ }
Example
fn main() {
for i64 i = 0 when i < 10 do i += 1 {
// loop body
} // loop will run 10 times
}
Arrays
Arrays have a completely unique syntax in Occult, but behave similarly to other C-like languages.
Syntax
array[/*size*/] /*datatype*/ name = { 1, "String", 1.43, x, /*etc.*/ };
Example
fn main() {
array[5] i64 arr = {1, 2, 3, 4, 5};
i64 sum = 0;
for i64 i = 0 when i < 5 do i += 1 {
sum = sum + arr[i];
}
return sum; // returns 15 (sum of the array)
}
Structs
Structs have a familiar syntax, which isn't hard to learn.
Take a node datatype for a linked list which stores i64.
struct node_i64 {
node_i64* next;
i64 data;
}
The syntax is fairly straightforward.
Accessing members
Using our node datatype, we can create a new node and then access the members like this.
struct node_i64 {
node_i64* next;
i64 data;
}
fn main() {
node_i64 new_node;
new_node.next = 0; // doesn't point to a new node
new_node.data = 10; // current node holds the value of 10
return new_node.data; // return our node's value (10)
}
-> syntax like C, there is only .Enums
Occult has enums. Enum variants start at 0 and auto-increment, or you can assign explicit integer values.
Syntax
enum /*Name*/ {
Variant,
Variant = /*value*/,
}
Example
include "../std/stdio.occ"
enum Color {
Red, // 0
Green, // 1
Blue // 2
}
enum Status {
OK = 200,
NotFound = 404,
ServerError = 500
}
fn main() i64 {
i64 c = Color::Green;
std::printf("Color: %i\n", c); // 1
std::printf("Status: %i\n", Status::OK); // 200
return 0;
}
Enum variants are accessed with the :: scope resolution operator. Enums can be used as struct field types and inside switch statements. They can also be used as a type, which will just default to an integer.
Modules
Occult has modules. A module groups related functions under a namespace, accessed with ::.
Syntax
module /*name*/ {
fn /*function*/() { }
}
Example
include "../std/stdio.occ"
module math {
fn add(i64 a, i64 b) i64 {
return a + b;
}
fn multiply(i64 a, i64 b) i64 {
return a * b;
}
}
fn main() i64 {
i64 sum = math::add(3, 4);
std::printf("3 + 4 = %d\n", sum);
i64 product = math::multiply(5, 6);
std::printf("5 * 6 = %d\n", product);
return 0;
}
Modules can be nested. Access nested modules by chaining ::, e.g. std::math::add(...).
Vessels
Occult has vessels. A vessel is like a struct but with methods. All fields are private by default and all methods are public. Inside a method, use self to access the vessel's own fields.
Syntax
vessel /*Name*/ {
/*type*/ /*field*/;
fn /*method*/(/*params*/) /*return type*/ {
// use self.field to access fields
}
}
Example
vessel Counter {
i64 value;
fn add(i64 delta) {
self.value += delta;
}
fn get() i64 {
return self.value;
}
}
fn main() i64 {
Counter c;
c.add(5);
c.add(7);
return c.get(); // 12
}
c.value) is a compile error. All interaction must go through the vessel's methods. More OOP-like features will be added to vessels over time, though Occult will intentionally stay relatively procedural in nature.Vessels can also be generic. Declare a generic parameter with generic on the line before the vessel.
include "../std/stdio.occ"
generic<T>
vessel Box {
T contents;
fn set(T val) {
self.contents = val;
}
fn get() T {
return self.contents;
}
}
fn main() {
Box<i64> b;
b.set(99);
std::print_integer(b.get()); // 99
std::print_string("\n");
}
Generics
Occult supports generics for structs, vessels, and functions. Declare a generic parameter with generic on the line before the struct or function. Multiple type parameters are supported.
Syntax
generic<T>
struct /*Name*/ {
T field;
}
generic<T>
fn /*name*/(T param) T {
return param;
}
Example
include "../std/stdio.occ"
generic<T>
fn identity(T val) T {
return val;
}
generic<T, U>
struct KeyValue {
T key;
U value;
}
fn main() {
std::printf("%i\n", identity<i64>(42));
KeyValue<i64, i32> kv;
kv.key = 10;
kv.value = 99;
std::printf("key=%i value=%i\n", kv.key, kv.value);
}
Instantiate generics at the call or declaration site by supplying the concrete type in angle brackets, e.g. identity or KeyValue.
Memory
This is one of the more unique parts about Occult. It uses a distinct syntax for pointers, and doesn't inherently behave the same as other languages.
Occult has a unique syntax for referencing, and dereferencing.
@ is used for referencing / address of, similar to C/C++'s &.
$ is used for dereferencing, same as doing *ptr in C/C++.
Simple Example
fn main() {
i64 v = 10;
i64* p = @v; // assign the address of `v` to `p`
$p = 30; // value of `v` is now 30
return v; // returns 30
}
Heap Allocation
Heap memory is managed through std::malloc and std::free from the standard library.
include "../std/stdio.occ"
fn main() {
i64 v = std::malloc(16);
$v = 100;
$(v + 8) = 200;
i64 total = $(v + 8) + $v; // 300
std::print_integer(total); // prints 300
std::print_string("\n");
std::free(v);
}
sizeof operator and doesn't automatically size to types like C does.Reference Parameters
Occult does have reference parameters similar to C++.
fn add(i64@ x_ref, i64 y_ref) {
$x_ref = $x_ref + y_ref; // you have to explicitly dereference the value to access it
}
fn main() {
i64 x = 10;
add(x, 30); // x is now 40 (x_ref has the address of x)
return x; // returns 40
}
Strings in Memory
There's a unique feature with strings - the size is stored statically in memory, so to write a strlen function we can literally just do this.
fn strlen_fast(string str) {
i64 len = $(str - 8);
return len;
}
The string length is stored 8 bytes before the string starts.
Standard Library
The Occult standard library is written in Occult itself and is still being actively developed. Include the files you need from the std/ directory.
How to include / import the modules.
import stdio from std;
import memory from std;
import vec from std;
import strlib from std;
// Alternatively
include "../std/stdio.occ"
include "../std/memory.occ"
include "../std/vec.occ"
include "../std/strlib.occ"
This is the end of the language specification for now, maybe check out the examples section to learn more!