Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ralix

Ralix is a simple, type-safe, tree-walking interpreter written in Rust. It is a hobby project, built to explore the concepts of building a programming language.

Features

  • Lexer: A simple and efficient lexer that scans the source code and produces a stream of tokens.
  • Parser: A Pratt parser that builds an Abstract Syntax Tree (AST) from the token stream.
  • Type Checker: A static type checker that verifies the correctness of the code before evaluation.
  • Evaluator: A tree-walking evaluator that traverses the AST to execute the code.
  • REPL: A Read-Eval-Print-Loop that allows for interactive programming.
  • CLI: A command-line interface to run scripts, inspect the AST, and use the REPL.

This book serves as the official documentation for the Ralix language and its interpreter.

Installation

Using cargo install

Important

For this step ensure you have the Rust toolchain installed.

You can install the ralix CLI directly from the source if you have the repository cloned.

cargo install --path .

Or if you don’t even care about the repository (like I usually do) you can quickly install ralix using:

cargo install ralix

After installation, you can run the ralix command from anywhere in your terminal:

ralix --version

Running from source

If you have the source code, you can also run the CLI directly using cargo run:

cargo run -- --help

This will build and run the latest version of the code. All the arguments to the ralix CLI should be passed after --.

Syntax

Ralix has a simple and familiar syntax, inspired by languages like Rust and C. This section provides a reference for the language’s syntax.

Types

Ralix is a statically typed language, and it comes with a set of built-in types.

Primitive Types

  • int: A 64-bit signed integer.

    int x = 10;
    
  • float: A 64-bit floating-point number.

    float y = 3.14;
    
  • bool: A boolean value, which can be true or false.

    bool is_active = true;
    
  • char: A single character.

    char initial = 'R';
    
  • str: A string of characters.

    str name = "Ralix";
    
  • null: A special type that has only one value, null. It is used to represent the absence of a value.

    int? a = null;
    

Composite Types

  • arr[T]: An array of elements of type T.

    arr[int] numbers = [1, 2, 3, 4, 5];
    
  • fn(...) -> T: A function that takes a sequence of arguments and returns a value of type T.

    let add = fn(int a, int b) -> int: a + b;
    
  • type[T]: A type that represents type T as a “type value”

    type[int] my_integer_ty = int;
    
  • map[K, V]: A Hash Map that holds K as keys and V as values.

    map[str, str] capitals = #{ "a": "A", "b": "B", c: "C" /* ... */}
    

Important

Let bindings auto binds the types

Special Types

  • type[T]: The type of a type. It is used to represent types as values.
  • T*: A pointer to a value of type T.
  • T?: A nullable type that can hold either a value of type T or null.
  • void: A type that represents the absence of a value. It is used as the return type of functions that do not return a value.
  • never: A type that represents a computation that never returns. It is used for functions that exit the program or run forever.
  • unknown: A special type that is used by the type checker when it cannot determine the type of an expression.

Statements

Statements are instructions that perform an action. In Ralix, statements only .

Binding statement

The binding statement is used to create a new variable binding. You can specify the type of the value. If you want it’s type to be specified automatically you can use the let statements

// Create a variable `x` of type `int` with a value of 5
int x = 5;

// Create a variable `y` and let the compiler infer its type
str y = "hello";

// Auto binds the type `float`
let pi = 3.14;

const statement

The const statement is used to create a new constant binding. Constants are immutable and must have their type specified.

// Create a constant `PI` of type `float`
const float PI = 3.14159;

Type Alias Statements

Using the type keyword you can create your own type aliases. Once a type alias defined it cannot be changed afterwards.

type MyStr = str?;
MyStr my_value = "hehe! I'm in danger!"; // Don't judge. This line came
                                         // to my mind for no reason

You also can use the types you got from the typeof expression. And also use them in binding statements.

#![allow(unused)]
fn main() {
const float PI = 3.14159;
let MyFloat = typeof PI;
MyFloat my_type_is_same_as_PI = 1.2;
}

fn statement

The fn statement is used to create a new function. Functions can have parameters and a return type. Optionally you can also bind const fn statements. This is allowed because functions are just binding statements that the value is just a regular function.

// Create a function `add` that takes two integers and returns an integer
fn add(int a, int b) -> int: a + b;

For flexibility you can use “type generics” in function parameters and return types.

#![allow(unused)]
fn main() {
fn first[T](arr[T] x) -> T? : x[0]
}

return statement

The return statement is used to exit a function and optionally return a value.

fn get_greeting() -> str: {
    return "Hello, Ralix!";
}

Expression statement

An expression statement is an expression that is followed by a semicolon. The value of the expression is discarded unless it’s the last expression statement that has been evaluated. This can be useful when using scope expressions.

// The function call is an expression statement
println("Hello, World!");

Assignment

An assignment statement is used to change the value of an existing variable.

int x = 5;
x = 10;

map[str, arr[int]] items = #{ "a": [0,1,2], "b": [3,4,5] };
items["b"] = [6,7,8];

arr[float] nums = [1.0, 2.7, 3.3, float(4)];
nums[2] = 5.8;

Important

Note that index assignment operations can only update existing values. If you wanna add a new value to a new hash-map using a key that hash-map isn’t using this operation will simply do nothing

Expressions

Expressions are constructs that produce a value.

Literals

Literals are the most basic type of expression. They represent a fixed value in the source code.

  • Integer literals: 10, _
  • Float literals: 3.14
  • Boolean literals: true, false
  • Character literals: 'a'
  • String literals: "hello"
  • Null literal: null
  • Array literals: [1, 2, 3]
  • Hash Map Literals: #{ "a": 1 , "b": 2 }

Identifiers

An identifier is a name that refers to a variable, constant, or function.

int x = 5;
int y = x; // `x` is an identifier

Prefix Expressions

A prefix expression has the operator before the operand.

  • - (negation): -10
  • ! (logical NOT): !true
  • * (dereference): *ptr
  • & (address of): &value
  • ~ (bitwise NOT): ~10

Infix Expressions

An infix expression has the operator between the operands.

  • Arithmetic: +, -, *, /
  • Comparison: ==, !=, <, >, <=, >=
  • Logical: &&, ||
  • Bitwise: |, ^, &
int x = 10;
int y = -4;

int sum = x + y;
bool are_equal = x == y;
int bit_or = x | y;

Note

Every int and float values in ralix are 64-bits and this cannot be changed. Why? Because I suck

Ralix follow C-style precedence which look like this:

Operator(s)Precedence
Default expr parsing precedenceLowest
||LogicalOr
&&LogicalAnd
|BitwiseOr
^BitwiseXOr
&BitwiseAnd
==, !=Equals
>, <, >=, <=LessGreater
>>, <<Shift
+, -Sum
*, /, %Product
!, -, *, ~Prefix
func(param)FunctionCall
hash_map["key"], Namespace::Item, Class.attributeAccess

if Expressions

An if expression allows for conditional execution. It must have an else block.

str result =
    if x > 5: "greater"
    else: "not greater"
;

Function Calls

A function call expression invokes a function with a list of arguments.

#![allow(unused)]
fn main() {
println("Hello, World!");
}

Index Expressions

An index expression is used to access an element of an array or a hash-map.

arr[int] my_arr = [1, 2, 3];
int first = my_arr[0];

Scope Expressions

A scope expression creates a new scope. The last expression in the scope is the value of the scope expression.

int y = {
    int x = 5;
    x + 1 // This is the return value of the scope
}; // x is dropped, y is 6

typeof Expressions

A typeof expression returns the type of a value.

int x = 10;
type[int] type_of_x = typeof x; // `type_of_x` is `type[int]`

Important

typeof expression only returns the type of the value during runtime. Example:

#![allow(unused)]
fn main() {
arr[int] my_arr = []; // Empty arrays automatically bind `unknown` type generic
typeof my_arr // `arr[unknown]`
}

Function Literals

A function literal is an anonymous function. When you want to bind a function you’d probably want to create a function statement instead

let add = fn(int a, int b) -> int: {
    return a + b;
};

run command

The run command is used to execute a Ralix script.

Usage

ralix run [FILE]

Arguments

  • [FILE]: The path to the script file to execute.

Description

The run command executes a Ralix script from a file. If no file is provided, it will attempt to run a project, but this feature is not yet implemented.

Examples

To run a script file named main.ralix:

ralix run main.ralix

repl command

The repl command starts the Read-Eval-Print-Loop (REPL), an interactive programming environment for Ralix.

Usage

ralix repl

Options

  • --tui: Use the experimental terminal user interface instead.

Description

The repl command allows you to enter and execute Ralix code interactively. The result of each expression is printed to the console.

There are two versions of the REPL:

  • The terminal user interface (TUI) for a richer interactive experience, which can be activated with the --tui flag.
  • The default legacy REPL is a simpler, line-by-line interpreter.

Examples

To start the default REPL:

ralix repl

To start the TUI REPL:

ralix repl --tui

ast command

The ast command parses a Ralix source file and prints its Abstract Syntax Tree (AST) in JSON format.

Usage

ralix ast [OPTIONS] <SOURCE_FILE>

Arguments

  • <SOURCE_FILE>: The path to the source file to parse.

Options

  • -o, --output <FILE>: The file to write the AST to. If not provided, the AST is printed to the console.

Description

The ast command is a useful tool for developers who want to inspect the structure of their code. It takes a source file, parses it, and then serializes the resulting AST into a pretty-printed JSON string.

Examples

To print the AST of a file named main.ralix to the console:

ralix ast main.ralix

To save the AST to a file named ast.json:

ralix ast -o ast.json main.ralix

meow command

A really important command :3

Contributors