Introduction

This is the Flint Wiki. You will have the whole Wiki, examples, beginners guide, syntax cheatsheet, and more in this book!

Installation

Just DO IT.

The Basics

In the basics, there are...well...the basics.

Single-line comment

A single line comment can easily be made with the // operator.

// This is a comment

Multi-line comment

A multi-line comment can easily be started with the /* operator and ended with the */ operator.

/* Multi-line
comment

*/

FlintDoc comments

To document you code, e.g. what a method is doing, you can use a YAML inspired structure to do so.

/**
Adds two numbers together

Params:
- x: The first number to add
- y: The second number to add

Returns:
- int: The sum of `a` and `b`

Author:
- Julius Grünberg
*/
def add(int x, y) -> int:
    return x + y;

From Beginner To Intermediate

In this guide, you will learn all the basics of Flint. For easier chapter management, the whole journey of "From Beginner to Master" is split into three parts. "From Beginner to Intermediate", "From Intermediate to Expert" and "From Expert to Master". With each chapter, you will learn more and more about the Flint language and its inner workings. At the end of this guide, you will know absolutely everything there is to know about the Flint language. No hidden features, no "never ending learning" of the language itself.

Flint is meant to be a language framework, which aims to be as easy as possible while being as powerful as possible. We aim to provide you with a box full of LEGOs, and at the end of this guide series, you will know every block that exists, and then your creativity is fully unhinged!

Flint is sometimes limited on purpose. It has no manual memory management capabilities, for example, but these limitations all serve a singular purpose: Making Flint easier and quicker to work with, but not letting it feel like magic. Every design choise of Flint has a purpose and at the end of this full guide, you will know every single one of them.

But enough talking, lets jump right into it, from 0 to mastery.

Hello World

def main():
    print("Hello, World");

The Hello World Program

Lets create a simple Hello World! program! The entry point of every Flint program is the main function. It returns an integer exit code. This function is reserved for the programs entry point, so no other function is allowed to be called main.

To define a function, we introduce the def keyword. Like Python, Flint does not use curly braces ({, }) to determine what is inside a function, but hard tabs (Tab, \t)!

The end of every line has to be maked by writing a semicolon ;.

With this out of the way, lets create a small simple Hello World program!

def main():
    print("Hello, World!");

The text between the " symbols is called a string (str) but this will be learned later, in the next chapter! For now, all we have to know is, that print is a function, which outputs whatever string it recieves to the console. The output of the above program would look like

Hello, World!

Compiling the Program

Save the above code into a file named hello.ft. ft is the file extension for Flint source files. They only contain code in written form.

To compile the .ft file to an executable file, we call

flint compile ./hello.ft -o ./hello

This will output the executable in the current working directory. It can be exexuted with the command

./hello

Comments in Flint

In Flint, comments are used to explain code or temporarily disable parts of it during debugging. Flint supports two types of comments:

  1. Single-line comments: Start with // and continue until the end of the line.
  2. Multi-line comments: Start with /* and end with */.

Here’s how you use comments:

def main():
    // This is a single-line comment explaining the print statement below
    print("Hello, Flint!");

    /*
     This is a multi-line comment.
     You can use it for detailed explanations
     or temporarily disabling multiple lines of code.
    */
    // print("This line is commented out and won't run.");

Comments are essential for writing clear, understandable, and maintainable code. Use them to explain your logic to others (or to yourself when revisiting code later).

Indentation in Flint

Flint enforces strict indentation rules to ensure clean and readable code. Only hard tabs (\t) are allowed for indentation. Each tab indicates a new level of nesting.

Let’s see why proper indentation is crucial:

def main():
print("This is not indented correctly.");

When you run the above code, you’ll see an error:

Error: Indentation expected for block inside main().

This happens because Flint expects all code inside main to be indented. Here’s the correct way to write it:

def main():
	print("This is correctly indented."); // Properly indented with a hard tab

Use comments to explain your code and highlight mistakes. For example:

def main():
// The following line is not indented and will cause an error:
print("Oops, this won't work!");

// Uncomment the next line to fix the indentation:
	// print("Now it works!");

Proper indentation is not just a stylistic choice in Flint—it’s a fundamental part of the syntax. By using only hard tabs, Flint ensures consistency across projects.


Now you’re ready to move on to variables and types, where we’ll dive deeper into how to store and manipulate data!

Variables and Types

Primitive Types

Primitive types are Flint's fundamental building blocks for representing data. They are the simplest forms of values you can use in your programs. Flint has the following primitive types:

  1. int Represents whole numbers, both positive and negative. Use int for counting, indexing, or whenever you need discrete values.
int x = 42; // A positive integer
int y = -15; // A negative integer
int z = 0; // Zero is also an integer
  1. flint Represents floating-point numbers (decimal numbers). Use flint for measurements, precise calculations, or any value that requires a fractional component.
flint pi = 3.14; // Approximation of π
flint temperature = -273.15; // Negative decimal values are valid
flint zero = 0.0; // Zero with a decimal
  1. str Represents a sequence of characters or text. Use str for names, messages, or any textual data. Strings must be enclosed in double quotes (").
str name = "Flint"; // A simple string
str empty = ""; // An empty string
str greeting = "Hello, World!"; // A common example

These types are the foundation of Flint's data handling. As you write more complex programs, you'll combine them in creative ways to represent and manipulate information.

Implicit Typing

Flint allows for implicit typing, meaning you don't always need to explicitly state the type of a variable. The compiler can infer the type based on the assigned value.

Here’s how it works:

x := 42; // The compiler infers that x is an int
pi := 3.14; // The compiler infers that pi is a flint
greeting := "Hello, Flint!"; // The compiler infers that greeting is a str

While implicit typing is convenient and makes code concise, explicit typing can improve readability in more complex programs. You can always use explicit typing if you want clarity:

int x = 42; // Explicitly declare x as an int
flint pi = 3.14; // Explicitly declare pi as a flint
str greeting = "Hello, Flint!"; // Explicitly declare greeting as a str

Use implicit typing for shorter, simpler programs and explicit typing when clarity is crucial.

String Interpolation

In Flint, you can embed variables or expressions directly into strings using string interpolation. This makes constructing strings with dynamic content both easy and readable. To interpolate, use the "Hello, my name is {name} and I am {age} years old.");


Output:

> Hello, my name is Flint and I am 1 years old.

You can interpolate any variable or expression into a string:

```rw
flint pi = 3.14;
print(<span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">&quot;</span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">eo</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal">p</span><span class="mord mathnormal">ii</span><span class="mord mathnormal">s</span><span class="mord mathnormal">a</span><span class="mord mathnormal">pp</span><span class="mord mathnormal">ro</span><span class="mord mathnormal">x</span><span class="mord mathnormal">ima</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord"><span class="mord mathnormal">p</span><span class="mord mathnormal">i</span></span><span class="mord">.&quot;</span><span class="mclose">)</span><span class="mpunct">;</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">//</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.02778em;">ser</span><span class="mord mathnormal">t</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">in</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">in</span><span class="mord mathnormal">t</span><span class="mopen">(</span></span></span></span>"2 + 2 equals {2 + 2}."); // Insert an arithmetic expression

Remember:

The print function only takes a single argument of type str.

Use string interpolation whenever you need to build strings dynamically—it’s clean, concise, and avoids manual concatenation.


Now that you understand Flint’s primitive types, implicit typing, and string interpolation, you’re ready to explore how to work with arrays and collections of data!

Control Flow: Making Decisions and Repeating Actions

The bool Type and Conditional Statements

Introduction to bool

In Flint, the bool type represents truth values: true or false. Boolean values are fundamental in programming, as they enable decision-making and control flow. For example:

bool is_learning = true;
bool is_hungry = false;

Here, isLearning is set to true, meaning it’s "on," while isHungry is false, meaning it’s "off."

The if Statement

The if statement lets your program execute code only when a condition evaluates to true. Here's how it works:

int age = 18;

if (age >= 18): // The condition evaluates to true
    print($"You are {age} years old, so you can vote!");

If the condition evaluates to false, the program skips the block of code inside the if.

Indentation Reminder: Remember to use hard tabs for indentation! Without proper indentation, Flint won't understand which code belongs to the if block.

The else Keyword

What if you want to handle both possibilities? That’s where else comes in:

int age = 16;

if (age >= 18): // If this is false...
    print($"You are {age} years old, so you can vote!");
else: // ...then this block executes
    print($"You are {age} years old, so you cannot vote.");

The else block runs only when the if condition is false.

The else if Keyword

Sometimes, you need multiple conditions. Instead of stacking multiple if statements, you can use else if to create a chain:

int age = 16;

if (age >= 65):
    print("You qualify for senior discounts.");
else if (age >= 18):
    print("You can vote but no senior discounts yet!");
else:
    print("You are too young to vote.");

Boolean Operations

Introduction

Boolean operators, such as and, or, and not, combine or modify bool values. They’re useful for creating more complex conditions. Here’s how each works:

  1. and Combines two conditions and evaluates to true only if both conditions are true.
bool is_adult = true;
bool has_id = false;

if (is_adult and has_id): // Both must be true
    print("You can enter.");
else:
    print("Access denied."); // Output: Access denied.
  1. or Combines two conditions and evaluates to true if at least one condition is true.
bool is_vip = true;
bool has_ticket = false;

if (is_vip or has_ticket): // Only one must be true
    print("You can enter."); // Output: You can enter.
  1. not Reverses the value of a bool.
bool is_raining = false;

if (not is_raining): // Turns false into true
    print("You don’t need an umbrella!"); // Output: You don’t need an umbrella!

Operator Precedence

and has a higher precedence than or, similar to how * has a higher precedence than + in arithmetic. Use parentheses to clarify expressions:

bool condition = true or false and false; // Evaluates to true (and happens first)
bool clarified = (true or false) and false; // Evaluates to false

Loops: Why Repetition Matters

Why Loops?

Programming often involves repeating tasks. For example, imagine printing every number from 1 to 10—it’s tedious to write print 10 times! Loops automate such repetition.

The for Loop

A for loop repeats a block of code for a specific number of iterations. Here’s the syntax:

for i := 0; i < 10; i++:
    print($"Iteration {i}");

Here’s what happens:

  1. i := 0; initializes i to 0.
  2. i < 10 checks if the condition is true. If not, the loop ends.
  3. i++ increments i by 1 after each iteration.

Enhanced for Loops

Why Enhanced Loops?

Enhanced loops are simpler when iterating over collections, like arrays or ranges. They’re especially useful for focusing on elements instead of indices.

Here’s the syntax:

for i, elem in 0..10:
    print($"Index {i}, Value {elem}");

i is the index (starts at -1).

elem is the current value.

Use enhanced loops when dealing with collections and ranges, but stick to normal loops for more complex conditions.

The range Type

What is a range?

A range represents a sequence of numbers. Use it in loops to iterate over specific values.

for i, elem in 5..10:
    print($"Index {i}, Value {elem}");

Output:

Index 0, Value 5
Index 1, Value 6
Index 2, Value 7
Index 3, Value 8
Index 4, Value 9
Index 5, Value 10

The Unused Operator _

Use _ to ignore either the index or the element:

for _, elem in 1..5:
    print($"Value {elem}"); // Ignores the index

for i, _ in 1..5:
    print($"Index {i}"); // Ignores the value

With these tools, you can now make decisions, repeat actions, and handle collections effectively in Flint!

Functions: Reusing Code and Organizing Logic

What is a Function?

Introduction

A function is a reusable block of code designed to perform a specific task. Functions make programs easier to read, debug, and maintain by encapsulating logic into manageable pieces. Think of a function like a recipe—you can reuse it to "cook" something multiple times without rewriting the steps.

Here’s how you declare a simple function in Flint:

def say_hello():
    print("Hello, world!");

The function above doesn’t take any arguments or return anything. You define it using the def keyword, followed by the function name and parentheses ().

Example: Calling a Function

Once a function is declared, you can "call" it to execute its logic:

def say_hello():
    print("Hello, world!");

def main():
    say_hello(); // Outputs: Hello, world!

Why Use Functions?

Functions allow you to:

  • Avoid repeating code.
  • Organize logic into clear, reusable blocks.
  • Make programs easier to read and debug.

Hint for Next Chapter:

While useful, functions are limited without the ability to take arguments. Imagine a function that prints a personalized greeting—how could you tell the function what name to use? We’ll explore that next.

Adding Arguments

What Are Arguments?

Arguments are variables passed to a function when it is called. They allow functions to operate on different data, making them far more versatile.

Example: A Function with One Argument

def greet(str name):
    print($"Hello, {name}!");

When calling the function, provide a value for the argument:

def main():
    greet("Alice"); // Outputs: Hello, Alice!
    greet("Bob");   // Outputs: Hello, Bob!

Multiple Arguments

Functions can take multiple arguments. Declare the arguments inside the parentheses, separated by commas:

def add_two_numbers(int a, int b):
    print($"The sum is {a + b}.");

Call the function by providing two values in the correct order:

def main():
    add_two_numbers(5, 7); // Outputs: The sum is 12.

Important Notes:

  1. The type of each argument matters. For example, if a and b are declared as int, you cannot pass str values.
  2. The order of arguments also matters. Always pass values in the same order as declared in the function.

What’s Next?

Arguments let you pass data into functions, but sometimes you want a function to give something back. That’s where return types come in.

Returning Values

Why Return Values?

Imagine you want a function to calculate the area of a rectangle. It’s not enough to just print the result—you may need to use the value elsewhere in your program. This is where returning values is essential.

Basic Return Example

Declare a return type after the -> symbol in the function header:

def get_greeting() -> str:
    return "Hello, Flint!";

When the function is called, it returns the value to the caller:

def main():
    str greeting = get_geeting();
    print(greeting); // Outputs: Hello, Flint!

Adding Arguments and Returning Values

Now let’s combine arguments with a return value:

def add_two_numbers(int a, int b) -> int:
    return a + b;

You can use the returned value in various ways:

def main():
    int result = add_two_numbers(10, 20);
    print($"The result is {result}."); // Outputs: The result is 30.

What’s Next?

Returning a single value is great, but what if a function needs to return multiple pieces of data? Flint supports this, as we’ll see in the next section.

Returning Multiple Values

Why Multiple Return Values?

Sometimes, a single return value isn’t enough. For instance, a function might need to calculate both the area and perimeter of a rectangle. Flint allows functions to return multiple values at once.

How to Return Multiple Values

Use parentheses () to group multiple values in the return statement and also use parentheses for declaring the return types:

def calculate_rectangle(int length, int width) -> (int, int):
    int area = length * width;
    int perimeter = 2 * (length + width);
    return (area, perimeter);

Accessing Multiple Return Values

When calling a function with multiple return values, use a group to store them:

def main():
    (int area, int perimeter) = calculate_rectangle(5, 3);
    print($"Area: {area}, Perimeter: {perimeter}.");

Output:

Area: 15, Perimeter: 16.

Important Notes:

  1. The types and order of the group must match the function’s return type.
  2. While the concept of "groups" is used here, it will be fully explained in a later chapter.

Encouragement:

You’ve now unlocked the full power of functions in Flint! From organizing logic to handling complex calculations, functions make your programs efficient and reusable. There’s still much to learn, but you’re already building the foundation for advanced concepts.

The journey has just begun—keep experimenting!

Why Data?

Flint is centered around data, making it the heart of its paradigm, Data-Object Convergence Paradigm (DOCP). In most programming languages, code revolves around objects or functions, but Flint is different—it places data at the center of everything. This design choice influences every aspect of the language.

Why Focus on Data?

  • Data is Everything: In real-world applications, data is what programs process, transform, and store. Focusing on data makes Flint naturally aligned with the goals of software development.
  • Simplicity and Modularity: By emphasizing data structures, Flint avoids complex object hierarchies, instead favoring simple, modular components. Best of All Worlds: Flint borrows strengths from other paradigms. From OOP, it takes the idea of encapsulated entities. From DOP, it inherits performance-centric, data-first design. Flint avoids the pitfalls of these paradigms by focusing on clear, maintainable, and efficient code.

Why is This a Good Thing?

Flint’s focus on data ensures programs are modular, cache-efficient, and easy to reason about. It reduces unnecessary abstractions while retaining flexibility, making it an ideal language for applications like game development, simulations, and more.

Enough Talk!

Now that you understand why data is so important, let’s dive into creating data structures in Flint. Shall we?

Declaring Data Modules

To define a new data module in Flint, use the data keyword. A data module consists of fields (the pieces of information it holds) and a constructor (which initializes those fields).

Basic Syntax:

data MyData:
    int x;
    int y;
    MyData(x, y);

What is a Constructor?

The constructor is the part of the data declaration that defines how to instantiate the data module. It must include all the fields defined in the data module.

Key Points:

  1. The constructor’s order determines how fields are initialized:
    • In the example, x must be assigned first, then y.
  2. The order of fields inside the module does not matter but should align with the constructor for clarity.
  3. Constructors are required for all data modules.

Example: Creating an Instance

def main():
    MyData d = MyData(10, 20);
    print($"d.x: {d.x}, d.y: {d.y}");
    // Outputs: d.x: 10, d.y: 20

Default Values

Sometimes, you may want a field to have a default value. In Flint, this is done by assigning a value to the field directly in its declaration.

Example: Default Values

data MyData:
    int x = 5;
    int y;
    MyData(x, y);

When instantiating this data module, you can use _ to signify using the default value for a field:

def main():
    MyData d = MyData(_, 20); // x uses the default value of 5
    print($"d.x: {d.x}, d.y: {d.y}");
    // Outputs: d.x: 5, d.y: 20

Key Notes:

  1. Default values simplify initialization but are optional.
  2. If a field doesn’t have a default value, using _ will result in a compiler error:
MyData d = MyData(_, _); // Error: y has no default value

Nested Data

Data modules can include other data modules as fields. This allows you to create nested structures, which are common in real-world programming.

Example: Nested Data

data Point:
    int x;
    int y;
    Point(x, y);

data Rectangle:
    Point top_left;
    Point bottom_right;
    Rectangle(top_left, bottom_right);

Usage:

def main():
    Point p1 = Point(0, 0);
    Point p2 = Point(10, 10);
    Rectangle rect = Rectangle(p1, p2);
    print($"rect.topLeft.x: {rect.topLeft.x}");
    // Outputs: rect.topLeft.x: 0

Circular References?

Flint does not allow a data module to reference itself directly or indirectly like showcased below:

data Node:
    int value;
    Node next;
    Node(value, next);

While this may seem restrictive, it prevents issues like infinite recursion or memory management problems. The explicit explaination of to why this is not directly possible will be explained in a later chapter. For now, focus on the fact that it is not possible.

Hint:

Flint can handle circular references with the help of the optional type (Opt). These convert a reference to a wek reference in circular context's, thus enabling the use of data in of itself, for example for linked lists.

Using Data in Functions

Data modules can be passed to and returned from functions, enabling you to manipulate them easily.

Example: Passing Data to Functions

def print_point(Point p):
    print($"Point(x: {p.x}, y: {p.y})");

def main():
    Point p = Point(3, 4);
    print_point(p); // Outputs: Point(x: 3, y: 4)

Example: Returning Data from Functions

def create_point(int x, int y) -> Point:
    return Point(x, y);

def main():
    Point p = create_point(5, 7);
    print($"Created Point: x={p.x}, y={p.y}");

By using functions with data, you can create and manipulate complex structures easily.

Conclusion

Data modules are the foundation of Flint’s design, allowing you to create, structure, and manage information effectively. From default values to nested structures, they provide a flexible yet powerful way to organize your program’s core logic. You’ve taken your first step into Flint’s data-centric world—congratulations!

Arrays: Organizing Data Sequentially

Introduction to Arrays

An array is a data structure that stores a collection of elements sequentially in memory. Arrays are useful for storing multiple values of the same type, such as numbers, strings, or even custom data modules. In Flint, arrays are immutable by default in terms of references—assigning one array to another always creates a copy, not a reference.

Key Points About Flint Arrays

  1. Arrays are always stored sequentially in memory, making access to their elements efficient.
  2. Arrays are value types in Flint. This means copying an array creates a new, independent copy of its data.
  3. If you modify an array inside a data object, you should access it directly using data.array instead of copying it out, as changes made to the copy won’t automatically reflect back in the original array.

Creating and Accessing Arrays

To declare a one-dimensional array, use the following syntax:

int[] arr; // Declare an uninitialized array
arr = int[5]; // Create an array with 5 elements initialized to the default value (0)

To assign values to specific elements or access them:

arr[0] = 10; // Set the first element to 10
int val = arr[0]; // Access the first element

Example: Using Arrays in Flint

def main():
    int[] arr = int[5]; // Create a 1D array of size 5
    arr[0] = 10;
    arr[1] = 20;
    print($"arr[0]: {arr[0]}, arr[1]: {arr[1]}");

Iterating Over Arrays

Often, you’ll want to process each element of an array. Flint supports two types of loops for working with arrays:

  1. Index-Based Loops: Ideal for accessing and modifying elements at specific indices.
  2. Enhanced For Loops: Useful for iterating over all elements with simpler syntax.

Using the Index-Based Loop

def main():
    // Initialize array of size 5 with values of 4
    arr := int[5](4);
    // Set each element to the double of the index
    for i := 0; i < 5; i++:
        arr[i] = i * 2;
    print($"arr[3]: {arr[3]}"); // prints 'arr[3]: 6'

Using the Enhanced For Loop

In enhanced for loops, you can access both the index (i) and the element (elem), as learned in chapter 4:

def main():
    int[] arr = int[5](6); // Initialize array
    for i, elem in arr:
        print($"Index: {i}, Value: {elem}");

Output:

Index: 0, Value: 6
Index: 1, Value: 6
Index: 2, Value: 6
Index: 3, Value: 6
Index: 4, Value: 6

Iterables in Flint

Flint arrays are iterable, meaning they can be used in enhanced for loops. Other iterables include ranges, which we’ll discuss later. Remember: the choice of loop depends on whether you need access to indices (i) or also elements (elem).

Hint: The type of the indices (i) is always of type uint (no signed int values) and the type of the elements (elem) is always the type of the array elements. If you create a str array (str[]), for example, elem will be of type str.

Multidimensional Arrays

Flint supports multidimensional arrays. In flint, these are always rectangle arrays, meaning that the length of each dimension is locked, unlike jagged arrays, which behave more like "array of arrays". Arrays are particularly useful for storing grid-like data, such as images or matrices.

Declaring Multidimensional Arrays

The number of commas ([,]) in the declaration indicates the number of dimensions:

int[,] arr; // 2D array
int[,,] cube; // 3D array

Initializing Multidimensional Arrays

To initialize a multidimensional array:

  1. Using Defaults:
int[,] arr = int[5, 5]; // 5x5 array with default value (0)
  1. Using a Specific Value:
arr := int[5, 5](4); // 5x5 array with all values set to 4

Accessing Multidimensional Arrays

Use indices for each dimension:

arr[1, 2] = 10; // Set the element at row 1, column 2
print(arr[1, 2]); // Access the element

Access Patterns with Ranges

Ranges allow you to access segments of an array or extract slices. This is especially powerful for creating subsets of arrays.

Using Ranges

  • Closed Ranges: Specify a start and end (both inclusive).
int[] slice = arr[2..4]; // Elements from index 2 to 4 (inclusive)
  • One-Sided Ranges: Leave one side open to indicate "to the start" or "to the end."
int[] slice = arr[3..]; // All elements from index 3 to the end
int[] slice = arr[..2]; // All elements from the start to index 2
  • Open Ranges: Open on both sides, indicating all elements.
int[] slice = arr[..]; // Equivalent to copying the whole array

Extracted Slices are Copies

When using ranges, Flint always creates a new array as a copy of the slice.

Multidimensional Access Patterns

Flint allows using ranges in multidimensional arrays, letting you extract slices easily. Fixed indices reduce the dimensionality of the result.

Examples:

  1. Extracting a Row:
int[] row = arr[2, ..]; // All elements in row 2
  1. Extracting a Column:
int[] column = arr[.., 3]; // All elements in column 3
  1. Extracting a Plane:
int[,] plane = cube[.., 2, ..]; // A 2D plane from a 3D array
// This returns a 2d array of all x and z values on index 2

Resulting Dimensionality:

  1. Fixing one dimension reduces the result by 1 dimension.
  2. Fixing all dimensions results in a single value:
int value = cube[1, 2, 3]; // Single value at [1, 2, 3]

Practical Example: Transposing a Matrix

// Transposing using range accessing
def transpose(int[,] matrix) -> int[,]:
    new_matrix := int[matrix[0, ..].length, matrix.length];
    for y := 0; y < matrix[0, *].length; y++:
        new_matrix[*, y] = matrix[y, *];
    return new_matrix;

// Transposing manually
def transpose(int[,] matrix) -> int[,]:
    /**
    The range operator has to be used here because there actually is no other way to
    find the length of the second dimension
    */
    new_matrix := int[matrix[0, ..].length, matrix.length];
    for x := 0; x < matrix.length; x++:
        for y := 0; y < matrix[0, ..].length; y++:
            new_matrix[y, x] = matrix[x, y];
    return new_matrix;

The slicing operation is not only cleaner for arrays, but it is more performant too, because multiple pieces of data can be copied simultaniously, whereas with manual indexing, each value is copied 1 by 1.

Arrays in Loops

Using ranges and slices in loops is incredibly powerful:

for i, elem in arr[2..4]:
    print($"Index: {i}, Value: {elem}");

Concurrency and Arrays

Flint’s range and array access patterns set the foundation for concurrent computations, enabling high performance. We’ll dive deeper into Flint’s concurrency model in later chapters.

Conclusion

Arrays are a core part of programming in Flint, allowing you to store and manipulate sequential data efficiently. From basic one-dimensional arrays to advanced multidimensional access patterns, Flint’s array system is powerful and intuitive. With this foundation, you’re ready to explore Flint’s entities, which bring even more flexibility to your data-centric applications. Let’s continue!

Entities: Organizing Behavior and Data

Entities in Flint are central to its philosophy of combining data and behavior in a modular and flexible way. They are similar to classes in object-oriented languages but follow Flint’s Data-Object Convergence Paradigm (DOCP). Entities consist of data modules to store information and func modules to define behavior, providing a balance of structure and modularity.

Monolithic Entities: The Basics

The simplest form of an entity in Flint is a monolithic entity, where data and functionality are defined together in one place.

Example:

entity Counter:
    data:
        int value;

    func:
        def increment(int amount):
            value += amount;

        def get_value() -> int:
            return value;

    Counter(value); // Constructor to initialize the entity

Key Points:

  1. Structure: Entities have two main sections:
    • Data: Variables that store the state of the entity.
    • Func: Functions that operate on the data.
  2. Constructor: The constructor initializes the entity. Its parameters must match the declared fields in the data section.
  3. Use Case: Monolithic entities are straightforward and perfect for small projects or tightly coupled logic.
  4. Immutability: The data saved inside an entity cannot be accessed outside of func modules. So, when Entity E uses a func module which acts on data D the data of D cannot be accessed via an instance of E. This means that e.value is impossible. For all operations on data, getters and setters must be provided inside the func module.

Usage Example:

def main():
    Counter counter = Counter(0);
    counter.increment(5);
    print($"Counter value: {counter.value}");
    counter.reset();
    print($"Counter value after reset: {counter.value}");

Monolithic entities are Flint’s simplest abstraction, but their true power emerges as we explore func modules.

Func Modules: Modularizing Behavior

Func modules separate behavior from entities, making Flint entities highly modular. They act upon data and can be reused across multiple entities. A func module defines functionality independently of a specific entity.

Syntax Recap:

data CounterData:
    int value = 0;
    CounterData(value);

func CounterActions requires(CounterData c);
    def increment(int amount):
        c.value += amount;

    def get_value() -> int:
        return c.value;

Key Points:

  1. Requires Keyword: The requires keyword specifies the data module(s) this func module operates on. In this case, it requires a data module of type Counter. This data module has to be present on every entity that uses the CounterActions func module.
  2. Reusability: A single func module can be shared across multiple entities that use compatible data structures.
  3. Immutability: Just like with monolithic entities, the data saved inside an entity cannot be accessed outside of func modules. So, when Entity E uses a func module which acts on data D the data of D cannot be accessed via an instance of E. This means that e.value is impossible. For all operations on data, getters and setters must be provided inside the func module.

By separating behavior from data, Flint encourages reuse and cleaner code. But how do these fit into entities?

From Monolithic to Modular Entities

A monolithic entity can be split into data and func modules, improving modularity and reusability.

Example:

data CounterData:
    int value;
    CounterData(value);

func CounterActions requires(CounterData c):
    def increment(int amount):
        c.value += amount;

    def get_value() -> int:
        return c.value;

entity Counter:
    data:
        CounterData;
    func:
        CounterActions;
    Counter(CounterData);

def main():
    Counter c = Counter(0);
    c.increment(5);
    print(c.get_value());

Key Points:

  1. Splitting Structure: Data and behavior are now split into distinct modules, improving maintainability and reducing duplication.
  2. Cleaner Logic: You can focus on either the data or behavior separately when extending or debugging the entity.

This separation shines when working with complex entities.

Additional Information:

When creating and using monolithic entities, the compiler inherently creates data and func modules for that entity. It keeps modular. The Counter example from chapter 8.1:

entity Counter:
    data:
        int value;

    func:
        def increment(int amount):
            value += amount;

        def get_value() -> int:
            return value;

    Counter(value); // Constructor to initialize the entity
``

will be converted by the compiler to this structure:

```rs
data DCounter:
    int value;
    DCounter(value);

func FCounter requires(DCounter d):
    def increment(int amount):
        d.value += amount;

    def get_value() -> int:
        return d.value;

entity Counter:
    data:
        DCounter;
    func:
        FCounter;
    Counter(DCounter);

This means that monolithic entites are being modularized internally. So there is no difference performance-wise when using very small monolithic or modular entities. However, as the entity becomes bigger and bigger, the performance gains from using modular entites become increasingly bigger.

So, when using very small entites or for quick prototyping, creating monolithic entites can be beneficial. But for bigger entities, please use the modular design.

Using Multiple Data and Func Modules

Entities can incorporate multiple data modules and func modules, making them powerful abstractions for complex systems.

Example:

data Position:
    int x;
    int y;
    Position(x, y);

data Velocity:
    int dx;
    int dy;
    Velocity(dx, dy);

func MovementActions requires(Position p, Velocity v):
    def move():
        p.x += v.dx;
        p.y += v.dy;

entity MovingObject:
    data:
        Position, Velocity;
    func:
        MovementActions;
    MovingObject(Position, Velocity);

Key Points:

  1. Combining Data: Entities can hold multiple data modules, each serving a specific purpose.
  2. Behavior Composition: Multiple func modules can act on the same data or different data modules within the entity.

This modular design fosters collaboration, where one developer can work on movement while another focuses on graphics.

It is generally not recommended to make any func dependant on many different data modules. The more dependent every func is on multiple different data modules, the less modular the whole system becomes. This would mean that the advantage DOCP has over OOP would diminish. It is not easy to keep function and data as modular as possible, but link help with that. Links are described in the next chapter and they greatly increase the possibilities of DOCP.

Introducing Links

Links allow you to connect func modules, enabling communication and coordination. This is achieved using the #linked flag and the -> operator in the link: section of the entity.

Syntax Recap:

// POSITION MODULES
data Position:
    int x;
    int y;
    Position(x, y);

func PositionUtils requires(Position p):
    def move():
        (dx, dy) := get_velocity();
        p.x += dx;
        p.y += dy;

    def get_position() -> (int, int):
        return (p.x, p.y);

    #linked
    def get_velocity() -> (int, int);
// VELOCITY MODULES
data Velocity:
    int dx;
    int dy;
    Velocity(dx, dy);

func VelocityUtils requires(Veclocity v):
    #linked
    def get_velocity() -> (int, int):
        return (v.dx, v.dy);
// MAIN ENTITY
entity MovingObject:
    data:
        Position, Velocity;
    func:
        PositionUtils, VelocityUtils;
    link:
        PositionUtils::get_velocity -> VelocityUtils::get_velocity;
    MovingObject(Position, Velocity);

def main():
    obj := MovingObject(10, 7, -2, 2);
    print(obj.get_position()); // prints '(10, 7)'
    obj.move();
    print(obj.get_position()); // prints '(8, 9)'

Key Points:

  1. Linked Functions: Functions marked with #linked must be linked in the link: section of the entity.
  2. Compilation Behavior: If multiple functions are linked, only the end-point function is included in the compiled program.
  3. Forced Override: When multiple functions with the same name and the same signature coexist in multiple func modules, they have to be linked together. The function at the end of the linking chain will be executed.
  4. Flags Required: Each function that is linked has to be marked with the #linked flag.

Links enable powerful abstractions and create cohesion across entities while keeping their implementations decoupled.

Extending Entities

The extends keyword allows you to create new entities that build upon existing ones. This is similar to inheritance in object-oriented programming.

Syntax Recap:

entity Movement:
    data:
        int x_pos = 0;
        int y_pos = 0;
        int x_vel = 0;
        int y_vel = 0;
    func:
        def move():
            x_pos += x_vel;
            y_pos += y_vel;
    Movement(x_pos, y_pos, x_vel, y_vel);

entity Player extends(Movement mv):
    data:
        int health = 100;
        int damage = 10;
    func:
        def recieve_damage(int amount):
            health -= amount;
    Player(mv, health, damage);

def main():
    mv := Movement(10, 10, 1, 2);
    player := Player(mv, 100, 10);
    player.move(); // Calling the function inherited by the Movement entity;

Key Points:

  1. Data and Func Inheritance: The new entity inherits all data, func and link modules from the parent entities.
  2. Extending Behavior: Additional functionality can be added without modifying the original entity.

Conclusion

Entities are Flint’s way of organizing and structuring both data and behavior. From monolithic entities to modular data and func modules, extending entities, and linking behavior, Flint entities provide the flexibility and power needed for modern, scalable applications.

Extending entities simplifies code reuse while keeping functionality modular. Together with the ability to link functions, a structure similar to "classic" OOP emerges, but the data is strictly separated, just like functions. Allthough links are very powerful, they can introduce headaches in concurrent environments. Use them with caution! As long as you use them in a readonly way (making a dummy function in the current func and linking it to the function in the func module which has access to the data, and only making it read the data, but never write it), you should see minimal to no problem with concurrency, which is talked about in the next chapter.

Up next: Concurrency, where we explore Flint’s capabilities for high-performance, parallel computing!

Building a Simple Program

Debugging Basics

From Intermediate to Expert

Concurrency

Entities are Flint’s way of organizing and structuring both data and behavior. From monolithic entities to modular data and func modules, extending entities, and linking behavior, Flint entities provide the flexibility and power needed for modern, scalable applications. Up next: Concurrency, where we explore Flint’s capabilities for high-performance, parallel computing!

Understanding Concurrency

Concurrency is the ability of a program to execute multiple tasks simultaneously, either by distributing them across multiple CPU cores or interleaving their execution on a single core. This capability has become critical in modern computing as CPUs have evolved from single-core processors to multi-core architectures.

Why Concurrency?

  • Performance Boost: By distributing tasks across cores, programs can complete faster.
  • Efficiency: Modern processors are optimized for parallelism; concurrency allows you to use their full potential.
  • Responsiveness: In applications like games or GUIs, concurrency enables smooth user interactions while background tasks run.

Challenges of Concurrency

Concurrency isn’t without its challenges. Some of the common pitfalls include:

  1. Race Conditions: When two or more threads access and modify the same data simultaneously, unexpected behavior can occur.
  2. Deadlocks: When threads wait on each other indefinitely, halting progress.
  3. Complexity: Managing threads and ensuring proper synchronization often leads to intricate, error-prone code.

How Flint Simplifies Concurrency

Flint’s design philosophy eliminates many of these issues:

  • Data-Centric Approach: By separating data and behavior, Flint ensures that each entity has its own data, avoiding race conditions by default.
  • Built-in Tools: Flint provides high-level abstractions for common concurrency patterns, allowing you to focus on your logic without worrying about low-level thread management.

While Flint makes concurrency easier, understanding the core principles ensures you can use it effectively.

Flint's Concurrency Features

Flint offers several built-in tools for concurrency, with run_on_all being a standout feature. These tools abstract away the complexity of managing threads, making Flint one of the easiest languages to adopt for concurrent programming.

run_on_all: The Basics

The run_on_all function distributes a task across all elements of an iterable or entities in a collection, running each task concurrently.

Syntax:

run_on_all(FUNC_OR_ENTITY_TYPE, FUNCTION_REFERENCE, [COLLECTION]);

How It Works

When an entity type is provided for the FUNC_OR_ENTITY_TYPE, the function given in the FUNCTION_REFERENCE will be executed on all entites of this type. This method is overloaded with another method, having one additional argument, the COLLECTION. A collection can be an array of entites or funcs, or a list of them.

Example:

entity SomeEntity:
    data:
        int value;
    func:
        def increment_value(int amount):
            value += amount;
    SomeEntity(value);

def main():
    arr1 := SomeEntity[100](SomeEntity(15));
    arr2 := SomeEntity[50](SomeEntity(25));

    // Increments the value of **ALL** existing entities of this type
    // This includes all entities from 'arr1' as well as all from 'arr2'
    run_on_all(SomeEntity, SomeEntity::increase_value(3));

    // Increments the value of all entities in 'arr1'
    run_on_all(SomeEntity, SomeEntity::increase_value(5), arr1);

Something important: A function reference is made via the :: symbols (double colon). Because Flint differentiates between function calls with . and function references with ::, the value with which the function is referenced can be bound to the function reference exaclty like a normal function call, instead of using .bind(...) or something similar to it. This greatly imroves the ease of use for function references.

Other Concurrency Functions

Flint provides additional concurrency functions, including:

  • map_on_all(FUNC_OR_ENTITY_TYPE, FUNCTION_REFERENCE, [COLLECTION]) -> List<ResultType>: Similar to the map function in other languages, map_on_all would run a specified function across all or some entities, depending if the collection argument is passed in or not. It collects all the results of the function in a new List. Here an example:
// Assuming that 'increase_value' returns the new value as an 'int'
List<int> results = map_on_all(SomeEntity, SomeEntity::increase_value(5));
  • filter_on_all(FUNC_OR_ENTITY_TYPE, FUNCTION_REFERENCE, [COLLECTION]) -> List<EntityType>: Similar to the filter function in other languages, filter_on_all executes a given predicate function and returns a list of all entities or funcs, where the predicate function resolved to true. Here an example:
// Assuming that the 'is_value_above' function exists and returns a 'bool'
List<SomeEntity> filtered = filter_on_all(SomeEntity, SomeEntity::is_value_above(5));
// This filtered list then can be used in other concurrent functions as well
// Concurrent functions are inherently modular and can be chained together to create powerful commands
run_on_all(SomeEntity, SomeEntity::decrease_value(5), filtered);

There exist several more concurrent functions, such as reduce_on_all, reduce_on_pairs or partition_on_all. Now that you understand how they work and how to use them, explore them by yourself!

  • run_in_parallel(func1, func2, ...): Runs multiple independent functions concurrently.
  • run_with_limit(func, collection, limit): Similar to run_on_all, but limits the number of concurrent tasks.
  • run_once(func): Ensures a function runs exactly once, even in concurrent contexts. <-->

Shared Data

While Flint’s design prevents race conditions by default, there are cases where data sharing between entities or threads is necessary. For these situations, Flint introduces shared data.

What is Shared Data?

Shared data is a special type of data module designed to be accessed by multiple threads or entities concurrently. Unlike regular data, shared data resides in the Heap, and Flint enforces synchronization to ensure safe access.

Syntax:

shared data SharedCounter:
    int value = 1;

As you may notice, shared data does not have a constructor. Why is that? Well, because it only exists once in the whole program. This also means that shared data cannot be initialized and used inside a variable. Its main purpose is to connect multiple entities, maybe of differnt type, where everyone of these entities can access and modify the data in a thread-safe way without much headache.

Imporant: Every field inside a shared data module must have default values set. This is because the module gets created with the programs startup.

Shared data houses mutex-checks by default, meaning that only a single thread can access the data at a time. The nice thing about shared data is, that inside this data module, more complex data types than only primitive types can be created too. This is because shared data cannot be initialized, and because a shared bit was introduced in DIMA (Dynamic Incremental Memory Allocation) (Flints memory management tool) (but you will learn more about it some time later).

Accessing Shared Data

When to Use Shared Data

  • Global State: For variables that must be shared across multiple entities or functions.
  • Coordination: When threads need to communicate through shared variables.

Example:

shared data PlayerScore:
    int highscore = 0;

// The PlayerScore p here is not an instance from but only a reference to the data module
func ScoreUtils requires(PlayerScore p):
    def set_highscore(int score):
        p.highscore = score;

    def get_highscore() -> int:
        return p.highscore;

entity ScoreManager:
    data: PlayerScore;
    func: ScoreUtils;
    // The PlayerScore is not allowed nor needed inside the constructor
    // because it is shared
    ScoreManager();

entity Player extends(ScoreManager m):
    data:
        HealthData;
    func:
        HealthUtils;
    // The ScoreManager does not have to be listed here because its constructor is empty
    Player(HealthData);

def main():
    // initializes a player with 100 health
    player := Player(100);
    print(player.get_highscore()); // prints '0'
    player.set_highscore(100));
    // because the ScoreManager is a regular entity, it can be instantiated too!
    manager := ScoreManager();
    print(manager.get_highscore()); // prints '100'
    manager.set_highscore(50);

    print(player.get_highscore()); // prints '50'

By default, Flint minimizes the need for shared data, promoting safer designs. Use shared data only when absolutely necessary. Use it sparingly because the additional mutex-checks to avoid race conditions make accessing shared data much slower compared to accessing "normal" data, especially in concurrent scenarios.

Conclusion

Flints concurrency approach, through its DOCP paradigm, is inherently simpler than in many other languages. Because Flint focuses on data modules themselves, their encapsulation and separation, it becomes much simpler to prevent race conditions or other problems related to concurrency.

You have learned a big chunk of Flints features by now. While you do not yet know everything, lets focus next on building a small program. Lets then focus on making a small library and publishing it on FlintHub to begin your collaborative Flint journey!

Errors & Enums (+ sets)

Optionals & Variants (+ ?)

File IO & Serialization

Entity Design Patterns

Using Libraries & FlintHub

Building a Moderate Program

Testing and Validation

Making Libraries & Publishing them

From Expert to Master

Generic Types

fn Type & Lambdas

Pipes

Concurrency 2

Advanced Memory Concepts (DIMA)

SIMD (Grouping + par_for)

Performance Optimization

system_call + C++