Language Specifications

Language Specifications for the Occult programming language.

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 *.occ files.

Keywords

Occult has keywords split into several categories: control flow, declarations, types, and machine-code-specific keywords.

Reserved Words

occult
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.

occult
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

occult
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

occult
fn
struct
vessel
enum
module
generic
const

Machine Code

occult
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

occult
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.

occult
// 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

occult
// 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

occult
"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.

Ignore the gaps in the numerical value of precedence, it doesn't mean anything.
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.

occult
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.

occult
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.

occult
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

occult
fn /*name*/(/*params*/) asm /*return type*/ {
    // x86-64 mnemonics
}

Example

occult
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.

occult
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.

occult
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.

occult
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.

occult
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");
}
The current __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

occult
/*target_type*/(/*expression*/)

Example

occult
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.

All of the comparison operators are listed earlier, as well as the unique quirks Occult has over other languages.

If Statements

Syntax

occult
if /*condition*/ { /*body*/ }
else if /*condition*/ { /*body*/ }
else { /*body*/ }
elseif is valid as well.
occult
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

occult
switch (/*expression*/) {
    case /*value*/: { /*body*/ }
    default: { /*body*/ }
}

Example

occult
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.

occult
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

occult
loop { /*body*/ }

Example

occult
fn main() {
  i64 x = 0;
  loop {
    x += 1;
  } // Infinitely add x by 1
}

While

While loops behave as you'd expect.

Syntax

occult
while /*condition*/ { /*body*/ }

Example

occult
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

occult
for /*variable decl*/ when /*condition*/ do /*iterator*/ { /*body*/ }

Example

occult
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

occult
array[/*size*/] /*datatype*/ name = { 1, "String", 1.43, x, /*etc.*/ };

Example

occult
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)
}
Arrays can't be returned from functions. Also, multidimensional arrays ARE supported.

Structs

Structs have a familiar syntax, which isn't hard to learn.

Take a node datatype for a linked list which stores i64.

occult
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.

occult
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)
}
There is no -> 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

occult
enum /*Name*/ {
    Variant,
    Variant = /*value*/,
}

Example

occult
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

occult
module /*name*/ {
    fn /*function*/() { }
}

Example

occult
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

occult
vessel /*Name*/ {
    /*type*/ /*field*/;

    fn /*method*/(/*params*/) /*return type*/ {
        // use self.field to access fields
    }
}

Example

occult
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
}
Fields are private - accessing them directly from outside the vessel (e.g. 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.

occult
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

occult
generic<T>
struct /*Name*/ {
    T field;
}

generic<T>
fn /*name*/(T param) T {
    return param;
}

Example

occult
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(42) or KeyValue kv;.

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++.

Pointers decay down to integers as of now, and aren't strictly typed. This will likely change in the future.

Simple Example

occult
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.

occult
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);
}
Occult doesn't have traditional pointer arithmetic - it goes byte-by-byte, and has no sizeof operator and doesn't automatically size to types like C does.

Reference Parameters

Occult does have reference parameters similar to C++.

occult
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.

occult
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.

occult
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"
The standard library is minimal and still under active development. More modules will be added over time.

This is the end of the language specification for now, maybe check out the examples section to learn more!

×