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 betrueorfalse.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 typeT.arr[int] numbers = [1, 2, 3, 4, 5]; -
fn(...) -> T: A function that takes a sequence of arguments and returns a value of typeT.let add = fn(int a, int b) -> int: a + b; -
type[T]: A type that represents typeTas a “type value”type[int] my_integer_ty = int; -
map[K, V]: A Hash Map that holdsKas keys andVas 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 typeT.T?: A nullable type that can hold either a value of typeTornull.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
intandfloatvalues 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 precedence | Lowest |
|| | LogicalOr |
&& | LogicalAnd |
| | BitwiseOr |
^ | BitwiseXOr |
& | BitwiseAnd |
==, != | Equals |
>, <, >=, <= | LessGreater |
>>, << | Shift |
+, - | Sum |
*, /, % | Product |
!, -, *, ~ | Prefix |
func(param) | FunctionCall |
hash_map["key"], Namespace::Item, Class.attribute | Access |
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
typeofexpression 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
--tuiflag. - 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
- devRals // Yeah only me :<