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 tranpsiler 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, 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 30 keywords in use right now, 16 reserved words and 14 words for raw datatypes.

Reserved Words

occult
if
elseif 
else
loop
when
do
break 
continue
while 
for
true
false
return
elseif can also be written as else if and it will have the same behavior.

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.

Unique

occult
shellcode
The shellcode keyword allows for the user to directly write shellcode in a function definition.

Misc

occult
include
struct

These are self explanatory, custom data structures, and including other files.

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

There is no += or -= (etc.) as of now. There also isn't ++ or --, so if you want to get the same behavior i = i + 1 would be it.

Control Flow, Functions, and Expressions

Occult has many types of expressions, and 2 different ways to declare functions.

Declaring a Function

Declaring a function in occult is fairly simple, the return type is optional, by default a functions 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, it functions 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.

Variables

As of right now, Occult only supports local variables, and there is no scope implemented in the compiler yet.

For example, here's a simple variable declaration then we return it in a function.

occult
fn main() {
  i64 v = 30;

  return v;
}

This code will just return 30.

Pretty much variable declarations work like they would in any other C-like language.

Calling a Function

Calling functions is also 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 if you want to get the same behavior.

Control Flow

There are 4 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

This is a basic example of if statements and some conditional behavior.

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;
  }
}

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 = x + 1;
  } // Infintely 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 = 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 = 0 when i < 10 do i = 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 = i + 1 {
    sum = sum + arr[i];
  }

  return sum; // returns 10 (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 linkedlist 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 .

Memory

This is one of the more unique parts about Occult, it uses a more unique 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 to doing *ptr in C/C++.

Just a quick note, pointers decay down to integers as of now, and aren't strictly typed, this will likely change, but Occult is still buggy, so that is why it hasn't changed yet.

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
}

More Complex Example

Allocates 16 bytes, stores two integers at different offsets (0 and 8), dereferences both to sum them (300), and frees the memory.

occult
fn main() {
  i64 v = alloc(16);

  $v = 100;

  $(v + 8) = 200; 

  i64 total = $(v + 8) + $v; // 300

  print_integer(total); // prints 300
  print_string("\n");

  del(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) { // uses prefix for string already builtin
  i64 len = $(str - 8); 

  return len;
}

The string length is stored 8 bytes before the string starts.

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

×