Rhenium is a statically typed, compiled language focused on simplicity, explicitness, and low-level control while maintaining a clean, Python-like syntax. Rhenium compiles to LLVM IR and provides native C++ integration for low-level and platform-specific code.
The language reference included with this project is production-ready and is itself an executable demonstration of the supported syntax and behavior.
- Compiled to LLVM IR
- Static typing with type inference
- Python-like, indentation-based syntax
- Immutable-by-default variables with explicit
mut - Structs, methods, constructors, and field access
- Generics and generic constraints
- Pointers, references, arrays, casts, and
sizeof - Lambdas and inline functions
if/else if/else,while,for, andmatch- Ranges and array operations
- Exceptions with
raise,try, andcatch - Async blocks with
.run()and.await() - Namespaces, type aliases, enums, traits, and struct inheritance
- Explicit resource cleanup with
delete zeroinitialization and deep-copy support withcopy- LLVM builtins and raw IR declarations
- Native C++ bindings and
externdeclarations - Module packages with
mod.reentry points andexport - Multiple
mainentry-point forms, including command-line arguments
func main() -> int:
println("Hello, World!")
count: mut = 0
while count < 3:
println(count)
count++
return 0
Modules are imported with using. Module names use dot notation, and self refers to the current input-file package.
using math
using list
using utils.helpers in "my_package"
using localModule in self
A directory can be imported as a cohesive module when it contains mod.re:
utils/
mod.re
helpers.re
strings.re
using utils
The compiler resolves that import to utils/mod.re when a directory module is selected. A mod.re file can re-export package members, and export is reserved for module package exports.
export utils
Imports are inherited transitively. If module A imports module B, then any module that imports A also has access to B and its exported members.
Rhenium can bind language-level declarations to compiler-provided LLVM implementations, embed raw LLVM IR, and load native C++ resources.
_Builtin func toggleAlarm(msg: string) -> none = """
LLVM Implementation
"""
_IR """
LLVM Top code declarations
"""
_NativeCPP("math") int add(a: int, b: int) and int sub(a: int, b: int) // used natively
extern("math") int add(a: int, b: int) // with math.cpp in the same folder
The reference demonstrates these type categories:
- Builtin types such as
int,bool,char,str, andnone - Struct types
- Pointer types (
ptr -> T) - Array types (
arr -> T) - Lambda types
- Range values
Type annotations are optional when the compiler can infer the type.
a: int = 10
b: long = 10
c: short = 10
d: byte = 10
e: float = 10.0
f: double = 10.0
g: bool = true
h: char = 'x'
i: str = "hello"
pointer_to_a: ptr -> int = ptr(a)
arr_int: arr -> int = [1, 2, 3]
Rhenium also provides type queries and type checks:
a_type: str = typeof(a)
is_int = a is int
Variables are immutable by default. Use mut when mutation is required.
x = 5
y: int = 10
z: mut = 15
func increment(x: mut int) -> int:
x = x + 1
return x
Global variables must be constant and cannot be mutable.
global PI_INT = 3
Rhenium supports binary operators, unary operators, ternary expressions, and prefix/postfix increment/decrement.
a = 5
b = 6
sum = a + b
diff = a - b
neg = -a
bitnot = ~a
max = a if a > b else b
x: mut = 12
y = x++
z = --x
Functions use func and may specify an explicit return type. Without an explicit return type, a function returns none.
func add(a: int, b: int) -> int:
return a + b
func log(msg: str):
println(msg)
Functions can be marked inline as a compile-time performance choice, and generic functions can be inlined as well.
func write(l: str) -> none inline:
println(l)
generic func sum<T>(a: T, b: T) -> T inline:
return a + b
Rhenium supports conditional branches, while loops, for loops, continue, break, and range iteration.
if a > 0:
println("positive")
else if a < 0:
println("negative")
else:
println("zero")
for i in range(0, 10):
println(i)
while a < 10:
a += 1
Range forms are:
range(<start>[, <end>[, <step>]])
Examples include range(5), range(0, 5), and range(0, 10, 2).
match provides multi-branch selection with a fallback _ case.
match (value):
1:
println("one")
2:
println("two")
_:
println("other")
Static arrays support indexing and assignment. len returns their length and can also dispatch to a struct's length method.
nums = [1, 2, 3]
first = nums[0]
nums[1] = 42
size = len(nums)
Dynamic arrays can be initialized explicitly and released with delete:
v = 5
// v must not be a constant,
// otherwise the compiler would compile this as a static array
dynarr = init arr -> int(v)
delete dynarr
Struct fields can be mutable or immutable. Instances are initialized with init, and methods are declared inside impl blocks.
struct Vec2:
x: mut int
y: mut int
impl Vec2:
init(x: int, y: int):
this.x = x
this.y = y
func length() -> int:
return this.x * this.x + this.y * this.y
func mul(v: int):
this.x *= v
this.y *= v
v = init Vec2(3, 4)
len2 = len(v)
v.mul(2)
Inside an implementation, this represents the dereferenced current struct pointer, while self represents the current struct pointer.
Pointers are explicit and can be created with ptr, dereferenced with @, cast between types, and inspected with sizeof.
rp = ptr(v)
dv = @rp
@rp = init Vec2(0, 0)
casted = cast<int>(a)
sz = sizeof(a)
The reference also demonstrates anyptr for a pointer to an arbitrary object.
Rhenium supports raise, try, and catch, including catching a specific error type and binding the raised value to a variable.
try:
raise "error"
catch str msg:
println("caught error: " + msg)
Custom errors can be built from Error and can provide a message() implementation.
The caught error can be a string or a struct inheriting Error.
struct TestError inherits Error:
msg: str
impl TestError:
func message() -> str:
return "Test Message \"" + msg + "\""
Async blocks run concurrently. A block can be started with .run() or awaited for a result with .await().
t = async:
println("running asynchronously")
t.run()
t2 = async(str):
return "Hello World!"
result = Thread::awaitAndCast<str>(t2) // awaits and cast the result pointer to a string
println(result)
Generic functions and structs use type parameters such as T.
generic func swap<T>(a: ptr -> T, b: ptr -> T):
tmp = @a
@a = @b
@b = tmp
a = 5
b = 6
swap(ptr(a), ptr(b)) // In this case the type is deducted by the compiler
println("a=" + a + ", b=" + b)
generic struct Box<T>:
content: T
b = init Box<int>(12)
Generic type parameters can also be constrained with traits.
Lambdas are first-class function values and are compiled as functions.
square = lambda(x: int) = x * x
v = square(2)
Namespaces group globals and functions and use :: for access.
namespace Alpha:
global I = -1
func beta():
println("called Alpha::beta()")
Alpha::beta()
Alpha::I
Type aliases create reusable names for existing types.
type IntPtr = ptr -> int
x: int = 5
x_ptr: IntPtr = ptr(x)
Enums can use implicit ordinal values or explicitly assigned constants.
enum Colors:
RED
BLUE
GREEN
r = Colors.RED
enum Cars:
MUSTANG = "Mustang"
FORD = "Ford"
FERRARI = "Ferrari"
Traits declare required functions. Structs can inherit one or multiple traits, and generic functions can constrain type parameters with inherits.
trait Vehicle:
func wroom()
struct Car inherits Vehicle:
name: str
impl Car:
func wroom(): // override
println("The car is running")
func wroomVehicle<T inherits Vehicle>(vehicle: T):
vehicle.wroom()
Multiple inherited traits are separated with commas:
struct Word inherits LetterContainer, Printable, Writeable:
letters: arr -> char
extern can declare functions that should not be name-mangled.
extern func write():
println("Hello World!")
A struct can define a delete block that behaves like a destructor when the object is explicitly deleted. Struct instances created inside a scope are also destroyed automatically when they fall out of scope.
struct CharBuf:
inner: mut ptr -> char
length: mut int
impl CharBuf:
delete:
if this.inner != null:
Memory::free(cast<anyptr>(this.inner))
this.inner = null
this.length = 0
func createBuf():
buf = init CharBuf("Hello World")
// Redundant: the compiler automatically calls delete when falling out of scope
//delete buf
zero initializes a variable without explicitly providing a value, and copy performs a deep copy.
i = zero int
j: CharBuf = zero // deducted type: CharBuf
func test() -> int:
return zero int // cannot deduct type in return
alpha = init CharBuf("Alpha")
copied = copy(alpha)
A Rhenium program can define a conventional main entry point:
func main() -> int:
println("RE language reference executed successfully")
return 0
The reference also demonstrates a command-line entry-point overload:
func main(argc: int, args: ptr -> str) -> int:
println("Specified " + argc + " args:")
for i in range(argc):
println(" - " + args[i])
return 0
RheniumLanguageReference.re is the comprehensive executable reference for the language. It demonstrates the parser-supported syntax and behavior across modules, declarations, types, expressions, control flow, data structures, memory management, error handling, concurrency, generics, namespaces, traits, enums, and entry points.
rhenium-0.0.1.vsix is the Rhenium VSCode extension. Place it into the extensions folder then install from vsix.
- Simple, readable syntax without hidden behavior
- Explicit control over memory and mutability
- LLVM-level extensibility
- Native integration where needed
- No implicit runtime magic
MIT License