001 - Fil

  • Author: Kevin Traini
  • Status: Published
  • Publication date: 2024-07-11
  • License: MIT

Abstract

Fil (thread, wire in French) is a programming language designed to be easy to use. It belongs mainly to functional and object-oriented paradigm. Its name is also a recursive acronym: Fil Is a Language (without the 'a' because it's ugly).

Table of Contents

1. Concept

Fil aims to be easy to use and totally not verbose. Syntax is as little as possible but complex enough to be able to do what you want. It belongs mainly to 2 paradigms (ordered by importance):

  • Functional: everything is expression and can be used as a value for another expression.
  • Object-oriented: data structures and encapsulation.

The language is strongly typed, this means there is no implicit conversion, you cannot assign an integer to a float.

1.1 Variables

It exists 2 ways to store values: variables and constant. The first is mutable but not the second.

Variables are pretty easy:

// One line declaration and definition
// The type is inferred by the compiler to i32
var my_variable = 2

// Two lines declaration then definition
// Note that you must declare its type as it cannot be inferred directly
var my_variable: i32
my_variable = 2

Constants are even easier:

// Constants can only be declared this way
// The type can be inferred by the compiler to f64 
val my_const = 3.1415

1.2 Basic types

There is some basic types we can call scalar types.

Integer

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch int uint

Float

There is f32 and f64 respectively for 32 and 64 bits floating point.

Boolean

The type is bool and can either be true or false.

Character

To store character you can use the type char. Under the hood it's an alias of the type u8.

Arrays

Arrays are static by default, their size are determined at compile time from the declaration. To do so just add brackets with the desired size (int[5]). The size can be inferred by the compiler if you associate declaration with definition:

var my_array = [1, 2, 3] // Inferred to i32[3]

Note that this array size cannot be increased nor reduced, you can only change the contained values as long as it's i32.

You can also define pointer to these types by adding a * just after (int* for example). To access to the value behind the pointer you can dereference it by putting a * before the variable name (*my_var for example). To retrieve the address of a variable you can use &my_var. To define a pointer you must use new, for example new i32(5) will create a pointer to the value 5.

val my_pointer = new i32(5) // type is i32*
*my_pointer // 5

1.3 Basic operators

Numeric operators

They apply only on integers, floats and chars.

// Addition
1 + 2
1++
// Soustraction
6 - 5
3--
// Multiplication
2.0 * 0.5
// Division
6 / 3
// Remainder
42 % 3

Boolean operators

// Logical AND
true && false
// Logical OR
true || true
// Logical NOT
!false

Comparison operators

// Equality
1 == 1
// Inequality
2 != 3
// Lesser than
2 < 3
2 <= 2
// Greater than
3 > 2
3 >= 3

Array operators

Arrays are basically just pointers, so you can do pointer arithmetics (at your own risk). Else you can use array accessor operator [].

Assignation operators

All binary numeric and boolean operators can be used directly for assignation, for example:

var my_var = 2
my_var += 3
// my_var == 5

1.4 Conditions

First type of control flow expressions are conditions. There is if and match:

if

if (a == b) {
  print("a equals to b")
} else if (a == c) {
  print("a equals to c")
} else {
  print("a has its own value")
}

match

match (my_var) {
  "hello" -> print("Hello")
  "world" -> {
    print("Hello ")
    print("World!")
  }
  _       -> print("Hi!") // Default case
}

The match expression can be very powerful, as you can match on array content or even data structure

match (my_array) {
  [1, 2, 3] -> print("array is sorted")
  _ -> print("array is not sorted")
}

match (someone) {
  Person("John", "Doe") -> print("Hi John!")
  Person(firstname, _)  -> printf("Hi %s!", firstname)
}

1.5 Loops

To iterate there is classic for loops with an index variable, but there is also for loops which iterates directly on an array.

// Classic for loop
for (var i = 0; i < 10; i++) {
  printf("Iteration %d", i)
}
// Loop on array
val my_array = [1, 2, 3]
for (val item: my_array) {
  printf("Item is: %d", item)
}

As all other things, loops are expressions, so it means you can use them as generator for example.

val my_array = for (var i = 0; i < 10; i++) i
// Is equivalent to
val my_array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.6 Functions

Functions are an important part of Fil, so they are easy to declare (note that declaration and definition cannot be separated).

fun sum(a: i32, b: i32): i32 {
  printf("Do the sum of %d and %d", a, b)
  a + b
}

This example function just do the sum of the two parameters, as you can see the return value of the function is the last expression of its body. For simple function like this one there is some shortest way to do it:

fun sum(a: i32, b: i32) (a + b)
// or
fun sum(a: i32, b: i32) = a + b

The return type of the function is not mandatory except for recursive function.

You can also store functions directly inside a variable and even pass them to a function. For that please use anonymous functions (or so-called lambdas). If you assign a classic function declaration to a variable, it will create an alias for this function.

val sum = (a: i32, b: i32) -> a + b
// The type of sum is: (i32, i32) -> i32
val result = sum(2, 3)

Parameters of functions can have default values (note that a parameter without default value cannot be after one with a default value).

fun mul(a: i32, b: i32 = 2) = a * b

And variadic parameters are also available. They must be the last one of the function. In that case the parameter can be used as an array.

from fil.algo use reduce // We'll see that later

fun sum(...operands: i32) = reduce(operands, (operand: i32, collector: i32) -> operand + collector, 0)

1.7 Data structures

Creating a data structure can be done in one line:

data Person

This structure is empty, but you can already use it.

var someone = Person()

You can add some properties and a constructor to it on the same line.

from fil.str use String // We'll see that later

data Person(val firstname: String, val lastname: String)

var someone = Person("John", "Doe")

Let's talk a little more about properties. When you declare them between parenthesis just after the data structure name, it also declares the default constructor of the structure. By declaring them with val you tell that the property is read-only. To be able to update it later use var. By default, the property is public and so visible to anyone, you can change this behavior by putting private in front of the property.

If you need additional properties not in the default constructor, you can add them afterward as well as other constructor:

from fil.str use String

data Person(val firstname: String, val lastname: String) {
  val birth_year: i32 = 2024

  Person(year: i32) {
    this.birth_year = year
  }
}

You can also add some behavior to default constructor.

from fil.str use String

data Person(val firstname: String, val lastname: String) {
  val birth_year: i32

  constructor {
    this.birth_year = 2024
  }
}

Members can be added the same way we declare function.

from fil.str use String

data Person(val firstname: String, val lastname: String) {
  fun getFullname() = this.firstname + " " + this.lastname
}

Let's take the example of Complex. Instead of adding function add, you can simply override function operator+:

data Complex(val real: f64, val imaginary: f64)

fun operator+(a: Complex, b: Complex) (
  Complex(a.real + b.real, a.imaginary + b.imaginary)
)

If some of your data structures share behavior, you can declare interfaces to implement or even extend some other data structure.

from fil.str use String

interface Animal {
  fun scream(): String
}

// Implement interface
data Cow: Animal {
  fun scream() = "Moo!"
}

// Extends structure
data LittleCow: Cow {
  fun scream() = "moo"
}

data BigCow: Cow {
  fun scream() = "MOOO!!"
}

Note that as declaration of a data structure is an expression, assigning it to a variable cause the variable to become an alias. It's the strange case where you can assign a type to a variable, this way you can also create aliases for all types.

val my_structure = data Person

val someone = my_structure()

1.8 Modules

Each Fil projects can be considered as module. The module name is a doted list of name (com.example.test for example). The standard library itself is a module.

A Fil module consist of a tree architecture of files and directories, the root can have a prefix and then each file will have a directive telling which part of the module it is:

mod my.module

When you divide your code into multiple files, each one will have its own module name (helping to avoid name collision). If you declare a function in one file, you can export it, and you use it in another file:

// Let's assume the project has my.module as prefix at the root directory
// sum.fil
mod my.module // Compiler consider the file as my.module.sum

export fun sum(a: i32, b: i32) = a + b

// main.fil
mod my.module // Compiler consider the file as my.module

from my.module.sum use sum

val result = sum(2, 3)

If you develop a library you can merge all your exports into one file and export them from it.

mod my.module

export from my.module.sum use sum

1.9 Genericity

Sometimes you would want to write a function which works with all types, but you cannot write alls by hand, it would be too long. For this case, there is genericity:

fun sum<T>(a: T, b: T) = a + b

This code define a function sum which works with all types as long as they have override operator+. Please note that both parameters still have the same type.

The <> just after the function name allow you to declare generic type name, it's a list of name, so you can declare as many as you want. You can use it for functions, but also for data structure.

from fil.collection use Pair, Vector

data Map<K, V>: Vector<Pair<K, V>>

As you can see here, when using a data structure or a function with a generic type, you must tell which type you want to use. For a function call the type can be inferred.

val result = sum(2, 3) // T is inferred to i32

// The four versions are valid
val map = Map<i32, Person>()
val map: Map = Map<i32, Person>()
val map: Map<i32, Person> = Map()
val map: Map<i32, Person> = Map<i32, Person>()

2. Tools

2.1 filc

Fil compiler. It takes a list of files and compile them into binaries. It can do cross-compilation for other architectures and also linkage to library developed in other languages (link to .so, .o, .a, .dll).

2.2 fmm

Fil module manager is a tool to help you use and share fil modules. It also manages your local fil environment by installing and updating both filc and standard library.

Through a config file, it can manage module dependencies, building, testing and publication to the registry.

2.3 Standard library

The standard library must as complete as possible. Users should find any basic utilities in it and even some complex one. Let's say that it must be as useful as c++ boost.

3. Development

All produced code must be open source under MIT license. The code must also be fully tested and available for as many platforms as possible.

Stable code is on master branch. Features are developed on other branches (or fork) then merged into master through a pull request. This pull request must be reviewed by at least the code owner, conversations resolved, tests all green. Each commit must be attached to an issue and the pull request too.

License

MIT License

Copyright (c) 2024-Present Kevin Traini

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.