Introduction

Ymir is a high-level, statically typed programming language designed to help developers to save time by providing strong and safe semantic. The semantic of this language is oriented towards safety, concurrency and speed of execution. These objectives are achieved thanks to its high expressiveness and its direct compilation into an efficient native machine language.

This documentation explores the main concepts of Ymir, providing a set of examples that demonstrate the strengths of this new language. It also presents an introduction to the standard library.

Important

Before starting to discuss the language, please keep in mind that it is still under development and that sometimes things may not work as expected. If you encounter errors that you do not understand or think are incorrect, please contact us at: gnu.ymir@mail.com. We look forward to receiving your mails!

Even more, all contributions are very welcome, whether to improve the documentation, to propose improvements to the language or std, to the runtime, or even to the automatic release generation procedure. All code repositories are available on github. In this documentation, known limitations of the language are sometimes highlighted, and calls for contribution.

Installation

The reference compiler of Ymir is based on the compiler GCC, which offer strong static optimization, as well as a vast set of supported target architectures.

This compiler can be installed on linux debian system, by following those simple steps:

  • First, you need to download the package :

Other gyc versions using other gcc backend versions are available at release.

  • And then, you need to install it using dpkg :
$ sudo dpkg -i gyc-11_11.3.0_amd64.deb


This package depends on :

  • g++-11
  • gcc-11
  • libgc-dev

If one of them is not installed, you will get an error, that can be resolved by running the following command :

sudo apt --fix-broken install


And then reinstall the package that has previously failed (dpkg). The compiler is now installed and is named gyc

$ gyc --version

gyc (GCC) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


Uninstallation

As for any debian package, the uninstall is done as follows :

$ dpkg -r gyc

Hello World

The following source code is the Ymir version of the famous program "Hello world !"

import std::io // importation of the module containing io functions

// This is a comment 

/** This is a function declaration
  * The main function, is the first one to be called
  */
def main () {
    // Print 'Hello World !!' to the console
    println ("Hello World !!");
}

A binary can be generated using GYC.

$ gyc hello.yr

This command produces a binary a.out that can be executed.

$ ./a.out
Hello World !!

The command line options of gyc are the same as those of all gcc suite compilers, with few exceptions that will be clarified in this documentation.

The option -o can be used to define the name of the output executable.

$ gyc hello.yr -o hello
$ ls
hello  hello.yr
$ ./hello
Hello World !!

Comments

Ymir offers different types of comments.

  • // A line of comment that stop at the end of the line

  • /* Multi-line comment that stops at the final delimiter */

def main () 
    throws &AssertError // Not what's important for the moment
{
    // This is an example of comment

    /* 
     * This is another example of comment
     * Where, the stars are optionnal
     */

    /*
    And this is the proof
    */

    // None of the comment lines have an influence on the compilation

    let x = 1 + /* 2 + */  3;
    assert (x == 4); 
}

In the above pogram, calling assert will throw an exception if the test is false. Errors are presented in the Error Handling chapter. For the moment, we can consider that the exception simply stops the program when the test fails.

We will see in the Documentation chapter, that comments are very usefull, to generate documentations.

Basic programming concepts

This chapter covers the basic concepts of Ymir programming language. Specifically, you will learn about variables, mutability, native types, functions and control flows.

Variables and Mutability

Variables are declared with the keyword let. The grammar of a variable declaration is presented in the following code block.

var_declaration := 'let' inner_var_decl (',' inner_var_decl)*
inner_var_decl  := (decorator)* identifier (':' type)? '=' expression
decorator := 'mut' | 'dmut' | 'ref'
identifier := ('_')* [A-z] ([A-z0-9_])*

The declaration of a variable is composed of four parts, 1) the identifier that will be used to refer to the variable in the program,

  1. the decorators, that will give a different behavior to the program regarding the variable, 3) a value, that sets the initial value of the variable, and 4) a type, optional part of a variable declaration, which when omitted is infered from the type of the initial value of the variable. Conversely, when specified the type of a variable is statically checked and compared to the initial value of the variable.

Variable type

The type of the variable, as presented in the introduction, is specified in the variable declaration. This implies a static typing of each variable, whereby a variable cannot change its type during its lifetime. To illustrate this point, the following source code declares a variable of type i32, and tries to put a value of type f32 in it. The language does not accept this behavior, and the compiler returns an error.

def main () {
    let mut x = 12; // 12 is a literal of type i32
    //  ^^^ this decorator, presented in the following sub section, is not the point of this example
    
    x = 89.0f; // 89.0f is a literal of type f32 (floating point value)
}

The compiler, because the source code is not an acceptable Ymir program, returns an error. The error presented in the following block, informs that the variable x of type i32, is incompatible with a value of type f32.

Error : incompatible types mut i32 and f32
 --> main.yr:(5,4)
 5  ┃ 	x = 89.0f; // 89.0f is a literal of type f32 (floating point value)
    ╋ 	  ^


ymir1: fatal error: 
compilation terminated.

Variable mutability

The decorators are used to determine the behavior to adopt with the variable. The keyword ref and dmut will be discussed in another chapter (cf. Aliases and References). For the moment, we will be focusing on the keyword mut. This keyword is used to define a mutable variable, whose value can be changed. A variable declared without the mut keyword is declared immutable by default, making its value definitive.

In another word, if a variable is declared immutable, then it is bound the a value, that the variable cannot change throughout the life of the variable. The idea behind default immutability is to avoid unwanted behavior or errors, by forcing the developpers to determine which variables are mutable with the use of a deliberately more verbose syntax, while making all the other variables immutable.

In the following source code a variable x of type i32 is declared. This variable is immutable, (as the decorator mut is not used). Then the line 7, which consist in trying to modify the value of the variable x is not accepted by the language, that's why the compiler does not accept to compile the program.

import std::io

def main () {
    let x = 2;	
    println ("X is equal to : ", x); 
    
    x = 3; 
    println ("X is equal to : ", x);
}

For the given source file, the compiler generates the following error. This error informs that the affectation is not allowed, due to the nature of the variable x, which is not mutable. In Ymir, variable mutability and, type mutability ensure, through static checks, that when one declares that a variable has no write access to a value, there is no way to get write access to the value through this variable. Although this can sometimes be frustrating for the user. We will see in a following chapter that sometimes setting a variable to immutable is not always sufficient, but there are some other ways to ensure that the value of a variable never changes.

Error : left operand of type i32 is immutable
 --> main.yr:(7,2)
    ┃ 
 7  ┃ 	x = 3; 
    ┃ 	^


ymir1: fatal error: 
compilation terminated.

The above example can be modified to make the variable x mutable. This modification implies the use of the keyword mut, which — placed ahead of a variable declaration — makes it mutable. Thanks to that modification, the following source code is an acceptable program, and thus will be accepted by the compiler.

import std::io

def main () {
    let mut x = 2;	
    println ("X is equal to : ", x); 
    
    x = 3; 
    println ("X is equal to : ", x);
}

Result:

X is equal to : 2
X is equal to : 3

In reality, mutability is not related to variables, but to types. This language proposes a complex type mutability system, whose understanding requires the comprehension of data types beforehand. In the following sections, we will, for that reason, present the type system, (and the different types of data that can be created in Ymircf. chapter Data types), before coming back to the data mutability, — and have a full overview of the mutability system in chapter Aliases and references.

Initial value

A variable is always declared with a value. The objective is to ensure that any data in the program came from somewhere, and are not initialized from a random memory state of the machine executing the program (as we can have in C language).

One can argue, that static verification can be used to ensure that a variable is set before being used, and argue that forcing an initial value to a variable is not the best way to achieve data validity. If at this point, this is more a matter of opinion than of sound scientific reasoning, we think that scattering the initialization of a variable, makes programs more difficult to read. More, immutable variables would be mutable for one affectation, making the behavior of a program even more difficult to grasp.

In the following table, is presented two examples of source code, with the same behavior. On the left, a valid source code accepted by the Ymir language, and on the right, a source code that is not accepted based on the arguments we put forward.

A B
import std::io;

def main () {
    let i = if (true) {
        42
    } else {
        7
    };
}
import std::io;

def main () {
    let i : i32;
    if (true) {
        i = 42;
    } else {
        i = 7;
    };
}

One can note from the left program, that an if expression has a value. Value computed by the result of the expression (in that case the value 42 of type i32). In point of fact, every expression can have a value in Ymir, removing the limitation, introduced by the forcing of an initial value to variables.

Global variables

Even if global variables have a rather bad reputation for many justified reasons, we choose to let the possibility to define them, since in spite of all, they allow some programmation paradigms that would be undoable otherwise.

Global variables are defined as any local variable, except that the keyword let is replaced by the keyword static. The following source code presents an utilization of an immutable global variable. This example is just a showcase, as the use of an enumeration (cf. Enum) would probably be more appropriate in this specific case.

import std::io

static pi = 3.14159265359

def main () {
    println ("Pi value is : ", pi);
}

All information presented on local variables are relevant to the case of global variables. Here, we are refering to static typing, mutability behavior, and default value initialization. No limitation exists on the value that can be stored inside a global variable, nor there exists on the nature of the initialization. Call of functions, conditional expressions, class initializations, etc., nothing was left out.

Global variables are initialized before the start of the program, before the call of the main function. To illustrate that, the following source code, creates a global variable of type i32 initialized from the value of the function foo. This function foo by making a call of the function println, prints a message to the console, and the main function also does it.

import std::io;

static __GLOBAL__ = foo ();

/**
  * This function print the message "foo", and returns the value 42
  */
def foo ()-> i32 {
  println ("foo");
  42
}

def main () {
    println ("__GLOBAL__ = ", __GLOBAL__);
}

Result:

foo
__GLOBAL__ = 42

Initialization order

There is no warranty on the order of initialization of global variables. This is probably, the first limitation that we can point out on the Ymir languages. Contribution, to allow such warranty would be very welcomed, but seems unlikely to be possible when global variables come from multiple modules (cf. Modules).

For the moment, because it is impossible to certify the good initialization of a global variable, before the start of the program, it is not allowed to initialize a global variable from the value of another global variable. However, this verification is very limited, as the value of a global variable can be used inside a function, and this same function used to initialize the value of another global variable. In the following source code, this behavior is illustrated.

static __A__ = 42;
static __B__ = __A__; 
static __C__ = foo ();

def foo () -> i32 {
    __A__
}

The compiler will unfortunetaly be able to see only the dependent initialization of __B__, and will let the initialization of __C__ from the function foo occur. Even if in that specific case, the dependency appears very clearly, it may not be that clear when the function foo come from an external module, that only provides its prototype.

Error : the global var main::__B__ cannot be initialized from the value of main::__A__
 --> main.yr:(2,8)
 2  ┃ static __B__ = __A__; 
    ╋        ^^^^^
    ┃ Note : 
    ┃  --> main.yr:(1,8)
    ┃  1  ┃ static __A__ = 42;
    ┃     ╋        ^^^^^
    ┃ Note : 
    ┃  --> main.yr:(2,16)
    ┃  2  ┃ static __B__ = __A__; 
    ┃     ╋                ^^^^^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Shadowing and scope

Lifetime

The lifetime of a variable is defined by a scope. Regrouping expressions separated by semi-colons between curly brackets, a scope is a semantic component well known in programming languages. It has some particularities in Ymir, but these particularities will be presented in forthcoming chapters (cf. Functions, Scope guards) and are not of interest to us at this point.

import std::io;

def main () {
    {
        let x = 12;
    } // x does not exists past this scope end
    println (x);
}

When a variable is declared inside a scope and is never used during its lifetime the compiler returns an error. To avoid this error, the variable can be named _. If it may seem useless to declare a variable that is not used, it can be useful sometimes (for example when declaring function parameters of an overriden function, cf. Class inheritence).

A variable whose name is _, is anonymus, then there is no way to retreive the value of this variable.

import std::io;

def main () {
    let _ = 12; // declare a anonymus variable
}

Shadowing

Two variables with the same name cannot be declared in colliding scopes, i.e. if a variable is declared with the name of a living variable in the current scope, the program is not acceptable, and the compiler returns a shadowing error. The following source code illustrates this point, where two variables are declared in the same scope with the same name x.

def main () {
    let x = 1;	
    let x = 2;	
    { 
        let x = 3; 
    }
}

The compiler returns the following error. Even the last variable in the scope opened at line 4 is not authorized. Many errors can be avoided, by simply removing this possibility. Possibility, in our opinion, that is not likely to bring anything of any benefit.

Error : declaration of x shadows another declaration
 --> main.yr:(3,9)
 3  ┃     let x = 2;    
    ╋         ^
    ┃ Note : 
    ┃  --> main.yr:(2,9)
    ┃  2  ┃     let x = 1;    
    ┃     ╋         ^
    ┗━━━━━┻━ 

Error : declaration of x shadows another declaration
 --> main.yr:(5,13)
 5  ┃         let x = 3; 
    ╋             ^
    ┃ Note : 
    ┃  --> main.yr:(2,9)
    ┃  2  ┃     let x = 1;    
    ┃     ╋         ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Global variables do not create variable shadowing problems on local variables. A global variable is a global symbol, and is accessible through its parent module definition (cf. Modules). Local variables on the other hand, are only accessible for the function in which they are declared. Symbol access gives the priority to local variables, behavior illustrated in the following example.

mod Main; // declaration of a module named Main

import std::io;

static pi = 3.14159265359

def main ()
    throws &AssertError
{
    {
        let pi = 3;
        assert (pi == 3); // using local pi
    } // by closing the scope, local pi does not exist anymore
    
    // because local pi does no longer exists
    // global pi is accessible
    assert (pi == 3.14159265359);
    
    // global pi can also be accessed from its parent module
    assert (Main::pi == 3.14159265359);
}

Primitives types

In Ymir language, each value has a certain type of data, which indicates how the program must behave and how it should operate with the value. Ymir is a statically typed language, which means that all types of all values must be known at compile time. Usually, the compiler is able to infer the data types from the values, and it is not necessary to specify them when declaring a variable. But sometimes, when it comes to the mutability of a variable or the inheritance of a class for example, the inference can be wrong and the behavior not adapted to what you might want to do.

Therefore, the type may be added when declaring a variable, as in the following code.

let mut x : [mut i32] = [1, 2];
let mut y = [1, 2];

To understand the difference between the type of x and the type of y, we invite you to read the chapter Aliases and References.

Each type has type attributes. Theses attributes are accessed using the double colon operator :: on a type expression.

let a = i32::init;  // i32 (0)

All primitive types have common attributes that are listed in the table below. Attributes can be surrounded by the token _, to avoid some ambiguity for some types (cf. Enumeration). For example, the attribute typeid is equivalent to __typeid__, or _typeid.

Name Meaning
init The initial value of the type
typeid The name of the type stored in a value of type [c32]
typeinfo A structure of type TypeInfo, containing information about the type

All the information about TypeInfo are presented in chapter Dynamic types.

typeof and sizeof

  1. The keyword typeof retreives the type of a value at compilation time. This type can be used in any context, to retreive type information. For example, in a variable declaration, a function parameter, or return type, structure fields, etc..
import std::io;

def bar () -> i32 {
    42 
}

def foo () -> typeof (bar ()) {
    bar ()
}

def main () {
    let x : typeof (foo ()) = foo ();
    
    println (typeof (x)::typeid, " (", x, ")");
}

Results:

i32 (42)
  1. The keyword sizeof retreive the size of a type in bytes at compilation time. It is applicable only on types, not on value, but the type of a value can be retreive using the typeof keyword. This size is given in a value of type usize (this scalar type is presented below).
import std::io;

def main () {
    let x : usize = sizeof (i32);
    println (x, " ", sizeof (typeof (x)));
}

Results: (on a x86-64 arch)

4 8

Scalar types

Scalar types represent all types containing a single value. Ymir has five primitive scalar types: integers, floating point, characters, booleans, and pointers. They can have different sizes for different purposes.

Integer types

An integer is a number without decimal points. There are different types of integers in Ymir, the signed one and the unsigned one. Signed and unsigned refers to the possibility for a number to be negative. Signed integer types begin with the letter i, while unsigned integers begin with the letter u. The following table lists all the different types of integers, and sorts them by memory size.

size signed unsigned
8 bits i8 u8
16 bits i16 u16
32 bits i32 u32
64 bits i64 u64
arch isize usize

The usize and isize types are architecture dependent, and have the size of a pointer, depending on the architecture targeted.

Each type of signed integer can store values ranging from -(2 n - 1) to 2 n - 1 - 1, where n is the size of the integer in memory. Unsigned types, on the other hand, can store numbers ranging from 0 to 2 n - 1. For example, type i8, can store values from -128 to 127, and type u8 can store values from 0 to 255.

An integer literal can be written using two forms, decimal 9_234 and hexadecimal 0x897A. The _ token, is simply ignored in a literal integer.

To make sure a literal value has the right type, a prefix can be added at the end of it. For example, to get a i8 with the value 7, the right literal is written 7i8. By default, if no prefix is added at the end of the literal, the language defines its type as a i32.

As indicated above, each type has attributes, the following table lists the integer-specific attributes:

Name Meaning
max The maximal value
min The minimal value

An overflow check is performed on literals at compilation time, and an error is returned by the compiler if the type of integer choosed to encode the literal is not large enough to contain the value. For example:

def main () {
    let x : i8 = 934i8;
}

Because a i8 can only store value ranging from -127 to 128, the value 934 would not fit. For that reason the compiler returns the following error.

Error : overflow capacity for type i8 = 943
 --> main.yr:(12,18)
    ┃ 
12  ┃     let x : i8 = 943i8;
    ┃                  ^^^

ymir1: fatal error: 
compilation terminated.

WARNING However, if the value cannot be known at compile time, the overflow is not checked and can lead to strange behavior. For example, if one try to add 1 to a variable of type i16 that contains the value 32767, the result will be -32768. Contribution: Provide a dynamic way to verify the overflow of arithmetic operation (at least in debug mode).

Floating-point types

Floating-point types, refer to numbers with a decimal part. Ymir provides two types of floating point numbers, f32 and f64, which have a size of 32 bits and 64 bits respectively.

Floating point literals are written as decimal followed by a point and then again followed by another decimal literal. One can omit the first literal if it is a 0 or the second one for the same reason. For example 7. is the same as 7.0 and .6 is exaclty the same as 0.6. However at least on of the two decimal literals must be written down, . is not a valid floating point literal. The prefix f can be written at the end of a floating point literal to specify that a f32 is wanted, instead of a f64 as it is by default.

def main () {
    let x = 1.0; 
    let y : f32 = 1.0f;
}

The following table lists the attributes specific to floating point types.

Name Meaning
init The initial value - nan (Not a Number)
max The maximal finite value that this type can encode
min The minimal finite value that this type can encode
nan The value Not a Number
dig The number of decimal digit of precision
inf The value positive infinity
epsilon The smallest increment to the value 1
mant_dig Number of bits in the mantissa
max_10_exp The maximum int value such that $$10^{max_10_exp}$$ is representable
max_exp The maximum int value such that $$2^{max_exp-1}$$ is representable
min_10_exp The minimum int value such that $$10^{min_10_exp}$$ is representable as a normalized value
min_exp The minimum int value such that $$2^{min_exp-1}$$ is representable as a normalized value

Boolean type

A boolean is a very simple type that can take two values, either true or false. For example:

def main () {
    let b = true;
    let f : bool = false;
}

The following table lists the attributes specific to boolean type.

Name Meaning
init The initial value - false

Character type

The c8 and c32 are the types used to encode the characters. The c32 character has a size of four bytes and can store any unicode value. Literal characters can have two forms, and are always surrounded by the token '. The first form is the character itself for example 'r', and the second is the unicode value in the integer form \u{12} or \u{0xB}.

As with literal integers, it is necessary to add a prefix to define the type of a literal. The prefix used to specify the type of a literal character is c8, if nothing is specified, the character type will be c32.

def main () {
    let x = '☺';	
    let y = '\u{0x263A}';
}

If the loaded literal is too long to be stored in the character type, an error will be returned by the compiler. For example :

def main () {
    let x = '☺'c8; 
}

The error will be the following. This error means that at least 3 c8 (or bytes) are need to store the value, so it doesn't fit into one c8 :

Error : malformed literal, number of c8 is 3
 --> main.yr:(2,10)
    | 
 2  | 	let x = '☺'c8; 
    | 	        ^

ymir1: fatal error: 
compilation terminated.

The following table lists the attributes specific to character types.

Name Meaning
init The initial value - \u{0}

Pointers

Pointer are values that stores an address of memory. They can be used to store the location of a data in memory. In Ymir, pointers are considered low level programming and are mainly used in the std, and runtime to interface with machine level semantics. One can perfectly write any program without needing pointers, and for that reason we recomand to not use them.

Pointers are defined using the token & on types, or on values. They are aliasable types, as they borrow memory (cf. Aliasable and References).

import std::io;

def main ()
    throws &SegFault, &AssertError
{
    let mut i = 12;
    let p : &i32 = &i; // creation of a pointer on i
    i = 42;
    assert (*p == 42); // dereference the pointer and access the value
}

Pointers are unsafe, and dereferencing a pointer can result in undefined behavior depending on where it points. It can also sometimes raise a segmentation fault. In Ymir, segmentation fault are recovered, and an exception is thrown. Error handling is presented in chaper Error Handling.

WARNING, Note that the segmentation fault may not occur even if the pointer is not properly set. The easiest way to avoid undefined behavior is to not use pointers and use std verified functions, or other semantically verified elements (cf Aliasable and References).

The following table lists the attributes specific to pointer types.

Name Meaning
inner The type of the inner value - for example : i32 for &i32

Compound types

Unlike scalar types, the compound can contain multiple values. There are three types of compounds: the tuple, the range and the arrays.

Tuple

A tuple is a set of values of different types. Tuples have a fixed arity. The arity of a tuple is defined at compilation time, and represent the number of values contained inside the tuple. Each element of a tuple has a type, and a given order. Tuples are built between parentheses, by a comma-separated list of values. A tuple of one value can be defined, by putting a coma after the first value. This way the compiler can understand the desire of creating a tuple, and do not confuse it with a normal expression between parentheses.

def main () {
    let t : (i32, c32, f64) = (1, 'r', 3.14);  // three value tuple
    let t2 : (i32,) = (1,); // single value tuple
    let t3 : i32 = (1); // single value of type i32
}

In the above example, the tuple t, is a single element, and can be used as a function parameter or as a return value of a function. It can also be destructured, to retrieve the values of its component elements. There are three ways of tuple destructuring.

  1. the dot operator ., followed by an integer value, whose value is known at compilation time. This value can be computed by a complex expression, as long as the compiler is able to retreive the value at compilation time (cf. Compilation time execution).
import std::io;

def main () {
    let t = (1, 'r', 3.14);
    let z : i32 = t._0;
    let y : c32 = t. (0 + 1); 
    println (t.2);
}
  1. the tuple destructuring syntax. This syntax, close to variable declaration, creates new variables that contains parts of the tuple that is destructured. In the following example, one can note that the tuple destructuring syntax allows to extract only some of the tuple values. The type of the variable e is the tuple (c32, f64), and its values are ('r', 3.14), when the variable f contains the value 1 of type i32.
def main () {
    let t = (1, 'r', 3.14);
    let (x, y, z) = t;
    
    let (f, e...) = t;
    println (f, " ", e.0);
}
  1. the keyword expand. this keyword is a compile-time rewrite, that expands the values of a tuple into a list of values. This list is then used to create other tuples, call functions, etc. The following example shows the use of the keyword expand to call a function taking two parameters, with the value of a tuple containing two values.
import std::io

def add (a : i32, b : i32) -> i32 
    a + b


def main () {
    let x = (1, 2);
    println (add (expand x)); 
    // ^^^^^^^^^^^^^^^^^^^^^^
    // Will be rewritten into : 	
    // println (add (x.0, x.1));
    
    let j : (i32, i32, i32) = (1, expand x);	
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // rewritten into : (1, x.0, x.1)
}

There is two other ways to destructurate a tuple. These ways are presented in forthcoming chapters. The following table lists the attributes specific to tuple types.

Name Meaning
arity The number of elements contained in the tuple (in a u32)
init a tuple, where each element values are set to init

Ranges

Ranges are types that contain values defining an interval. A range is named r!T, where T is the type of the range limits. They are created by the token .. and .... A range consists of four values, which are stored in the fields shown in the following table. These fields can be accessed using the dot operator ..

name type value
fst T the first bound
scd T the second bound
step mut T the step of the interval
contain bool Is the scd bound contained in the interval ?
def main () {
    let range : r!(i32) = 1 .. 8; 	
    let c_range : r!(i32) = 1 ... 8;
}

The step_by function takes a range as a parameter and returns a new range, with a modified step. This function is a core function, thus there is nothing to import.

def main () { 
    let range = (1 .. 8).step_by (2); 
} 

The Control flows section shows a use of these types.

Arrays

An array is a collection of values of the same type, stored in contiguous memory. Unlike tuples, the size of an array is unknown at compile time, and in Ymir, they are similar to slices, and will be refered as such. Slices are defined with a syntax close to the one of tuple, but with brackets instead of parentheses, for example [1, 2, 3]. The type of a slice is also defined using the brackets, for example [i32], meaning a slice containing i32 values.

String literals, enclosed between double quotes are a special case of slice literals. There is no string type in Ymir, but only slices type. Because of this, string values are typed [c32] or [c8] depending on the type of values contained in the slice. String literals can be prefixed with the keyword s8 or s32 to define the encoding system used. By default, when no prefix is specified a string literal have a type [c32].

import std::io;

def main () { 
    let x = [1, 2, 3]; // a [i32] slice
    let y = "Hello World !!"; // a [c32] slice
    let z = "Hello World !!"s8; // a [c8] slice
}

Warning: The length of a [c8] literals can seem incorrect due to the encoding system. For example, the slice "☺"s8 have a length of 3. To summarize, [c8] slices are utf-8 encoded string literals, when [c32] are encoded in utf-32.

A slice is a two-word object, the first word is the length of the slice, and the second is a pointer to the data stored in the slice. A slice is an aliasable type, its mutability is a bit more complicated than the mutability of scalar types (except pointers), because it borrows memory which is not automatically copied when an assignment is made. This section will not discuss the mutability of internal types or aliasable types. This is discussed in the chapter Aliases and References.

The field len records the length of the slice and can be retrieved with the dot operator .. The length of the slice is stored in a usize field.

import std::io
 
def main () {
   let x = [1, 2, 3];
   println ("The value of x : ", x); 
   println ("The length of x : ", x.len);
}

Similarly, the ptr field, gives access to the pointer of the slice and its types depend on the inner type of the slice, and is never mutable. Pointer type are absolutely not important for a Ymir program, and we suspect that you will never have use of them. They are defined to allow low level programming paradigms, and are used in the std and runtime.

To access the elements of an array, the [] operator is used. It takes either an integer value or a range value as parameter. If a range value is given, a second slice that borrows a section of the first is created. For now, the step of the range does not affect the borrowing of the array. Contribution can be made here. On the other hand if an integer value i is given as parameter, the value at the index i is returned.

import std::io;

def main () 
    throws &OutOfArray 
{
    let x = [1, 2, 3];
    let y = x [0 .. 2];
    let z = x [0];
    
    println (x, " ", y, " ", z); 
}

The length of a slice is unknown at compilation time, and access can be made with dynamic integers whose values are also unknown at compilation time. For that reason, it may happen that the parameters used go beyond the slice length. With this in mind, slice access is considered unsafe, and can throw an exception of type &OutOfArray. The exception system, and error handling is detailed in the chapter Error Handling.

Slices can be concatenated, to form another slice. The concatenation is made using the operator tilde on two operands. To work properly and be accepted by the language, the two slice used as operands must share the same type (but not necessarily mutability level, the mutability of the operand with the lowest mutability level is choosed for the result of the operation cf. Aliases and References).

import std::io

def foo () -> [i32] {
    [8, 7, 6]
}

def main ()  {
    println ([1, 2, 3] ~ foo ());
}

Results:

[1, 2, 3, 8, 7, 6]

The tilde token was chosen to avoid some ambiguity. In some languages such as Java, the concatenation is made using the token + that sometimes creates some ambiguity when concatenating strings, and other elements such as integers. For example, the expression "foo" + 1 + 2, is ambiguous. One can note however, that since concatenation only works on slices of the same type, the following expression "foo" ~ 2, is invalid as "foo" is of type [c32], and 2 of type i32.

Another syntax can be used to create slices. This syntax called slice allocation, allocates a slice on the heap and set the same value to every element of the slice.

import std::io
import std::random;

def main () {
    let a : [i32] = [0 ; new 100u64]; // this avoids the write of 100 zeros
                                      // but the result is the same
                              
    let b = [12 ; new uniform (10, 100)]; 
    //                ^^^^^^^ generates a random value between 10 and 100
    println (a, " ", b);
}

The following table lists the attributes specific to slice types.

Name Meaning
inner the inner type
init an empty slice (with len == 0us)

Static Arrays

Unlike the slice, static arrays are stored in the stack rather than on the heap. To be possible, their size must be known at compilation time. The syntax used to create a static array is close to the syntax of a slice allocation, but the keyword new omitted.

import std::io

/**
  * Takes an array of size twelve as parameter
  */
def foo (a : [i32 ; 12]) {
    println (a);
}

def main ()
    throws &OutOfArray
{
    let mut a : [mut i32 ; 12] = [0 ; 12];

    for i in 0 .. 12
        a [i] = i

    let b = [1; 12];

    foo (a);
    foo (b);
}

A static array can be transformed into a slice using the alias, copy and dcopy keywords. The chapter Aliases and references explains the difference between these keywords.

import std::io

def main () {
    let x : [i32; 12] = [0; 12];
    
    let a : [i32] = alias x;
    let b = copy x;
    
    println (a, " ", b);
}

One can argue that slice literals should be of static array type. We made the choice to create slices from array literals rather than static arrays to avoid verbosity, as we claim that slices are way more commonly used than arrays with a static size. We are for the moment considering the possibility to prefix slice literals, to define static array literals, but the question is not yet decided.

The following table lists the attributes specific to array types.

Name Meaning
inner the inner type
len the len of the array (usize value)
init an array where each element is set to the init value of the inner type

Option

The option typed values are values that may be set or not. They are defined using the token ? on types or values. Further information on option type are given in the chapter Error handling, as they are completely related to error management system.

import std::io;

def main () {
    let i : i32? = (12)?; // an option type containing the value 12
    let j : i32? = (i32?)::err; // an option value containing no value
}

The value of an option type can be retreived using functions in the std, or pattern matching. In this chapter, we only focus on the unwrap function, pattern matching being left for a future chapter (cf. Pattern matchin). The function unwrap from the module std::conv, get the value contained inside an option type. If no value is contained inside the option, the function throws an error of type &CastFailure.

import std::io;
import std::conv;

def foo (b : bool)-> (i32)? {
    if b { 
        19? // return the value 19, wrapped into an option
    } else {
        (i32?)::__err__ // return an empty value
    }
}


def main () 
    throws &CastFailure 
{
    let x = foo (true);
    println (x.unwrap () + 23);
}

The following table lists the attributes specific to option types.

Name Meaning
err An empty option value

Cast

Some value can be transformed to create value of another type. This operation is done with the cast keywords, whose syntax is presented in the code block below.

cast_expression := 'cast' '!' ('{' type '}' | type) expression

In the following example, a cast of a value of type i32 to a value of type i64 is made. As said earlier, implicit casting is not allowed. The mutability of the casted value is always lower or equal to the mutability of the original value (for obvious reason). Warning cast can cause loss of precision, or even sign problems.

let a = 0;
let b : i64 = cast!i64 (a);

The following table list the authorized casts of the primitive types :

From To
i8 i16 i32 i64 isize i8 i16 i32 i64 u8 u16 u32 u64 isize usize
u8 u16 u32 u64 usize i8 i16 i32 i64 u8 u16 u32 u64 isize usize c8 c32
f32 f64 f32 f64
c8 c8 c32 u8
c32 c8 c32 u32
&(X) for X = any type &(void)

Cast if a very basic type transformation, and must be used with precaution for basic operations. We will see in a forthecoming chapter (cf. Dynamic conversion) a complex system of conversion, provided by the standard library. This conversion system can be used to transform value of very different type and encoding.

Functions

Function is a widely accepted concept for dividing a program into small parts. A Ymir program starts with the main function that you have already seen in previous chapters. All functions are declared using the keyword def followed by a identifier, and a list of parameters. A function is called by using its identifier followed by a list of parameters separated by commas between parentheses.

import std::io 

/** 
 * The main function is the entry point of the program
 * It can have no parameters, and return an i32, or void
 */
def main () {
    foo ();
}


/** 
 * Declaration of a function named 'foo' with no parameters 
 */
def foo () {
    println ("Foo");
        
    bar (); 
}


/**
 * Declaration of a function named 'bar' with one parameter 'x' of type 'i32'
*/
def bar (x : i32) {
    println ("Bar ", x);
}


The grammar of a function is defined in the following code block.

function := template_function | simple_function
simple_function := 'def' identifier parameters ('->' type)? expression
template_function := 'def' ('if' expression) identifier templates parameters ('->' type)? expression

parameters := '(' (var_decl (',' var_decl)*)? ')'
var_decl := identifier ':' type ('=' expression)?

identifier := ('_')* [A-z] ([A-z0-9_])*


The order of declaration of the symbol has no impact on the compilation. The symbols are defined by the compiler before being validated, thus contrary to C-like languages, even if the foo function is defined after the main function (in the first example of this chapter), it's symbol is accessible, and hence callable by the main function. Further information about symbol declarations, and accesses are presented in chapter Modules.

Parameters

The parameters of a function are declared after its identifier between parentheses. The syntax of declaration of a parameter is similar to the syntax of declaration of a variable, except that the keyword let is omitted. However, unlike variable declaration, a parameter must have a type, and its value is optional.

import std::io 

/**
 * Declaration of a function 'foo' with one parameter 'x' of type 'i32'
 */
def foo (x : i32) {
    println ("The value of x is : ", x);
}

/**
 * Declaration of a function 'bar' with two parameters 'x' and 'y' whose respective types are 'i32' and 'i32'
 */
def bar (x : i32, y : i32) {
    println ("The value of x + y : ", x + y);
}

def main () {
    foo (5); // Call the function 'foo' with 'x' set to '5'
    bar (3, 4); // Call the function 'bar' with 'x' set to '3' and 'y' set to '4'
}


Default value

A function parameter can have a value, that is used by default when calling the function. Therefore it is optional to specify the value of a function parameter that have a default value, when calling it. To change the value of a parameter with a default value, the named expression syntax is used. This expression, whose grammar is presented in the following code block, consists in naming a value.

named_expression: Identifier '->' expression


The following source code presents an example of function with a parameter with a default value, and the usage of a named expression to call this function.

import std::io


/**
 * Function 'foo' can be called without specifying a value for parameter 'x'
 * '8' will be used as the default value for 'x'
 */
def foo (x : i32 = 8) {
    println ("The value of x is : ", x);
}

def main () {
    foo (); // call 'foo' with 'x' set to '8'
    foo (x-> 7); // call 'foo' with 'x' set to '7'
}


The named expression can also be used for parameters without any default value. Thanks to that named expression, it is possible to specify the parameter in any order.

import std::io


/**
 * Parameters with default values, does not need to be last parameters
 * This function can be called with only two parameters ('x' and 'z'), or using named expression syntax
 */
def foo (x : i32, y : i32 = 9, z : i32) {
    println (x, " ", y, " ", z);
}

def main () {
    // Call the 'foo' function with 'x' = 2, 'y' = 1 and 'z' = 8 
    foo (8, y-> 1, x-> 2);
    foo (1, 8); // call the function 'foo' with 'x' = 1 and y = '9' and z = '8'
}


Results:

2 1 8
1 9 8


Any complex expression can be used, for the default value of a function parameter. The creation of an object, a call of a function, a code block, etc. The only limitation is that, you cannot refer to the other parameters of the function. Indeed, they are not considered declared in the scope of the default value.

def foo (x : i32) -> i32 { ... }
def bar (x : i32) -> i32 { ... }

/**
 * Declaration of a 'baz' function, where 'b' = bar(1) + foo(2), as a default value
 */
def baz (a : i32, b : i32 = {bar (1) + foo (2)}) {
 // ...
}

def main () {
    baz (12);
}


The symbols used in the default value of a parameters must be accessible in the context of the function declaration. In the last example, that means that the function baz must know the function bar and the function foo, however, there is no need for the function that calls it (here the function main) to know these symbols. Further explanation on symbol declarations and accesses are presented in chapter Modules.

Recursive default value


Recursivity of default parameter is prohibited. To illustrate this point, the following code example will not be accepted by the compiler.

import std::io;

def foo (foo_a : i32 = bar ()) -> i32 {
                    // ^^^ here there is a recursive call 
    foo_a
}

def bar (bar_a : i32 = foo ()) -> i32 {
                    // ^^^ recursivity problem
                     
    println ("Bar ", bar_a);
    foo (foo_a-> bar_a + 11) 
}

def main () {
    println ("Main ", bar ()); // no need to set bar_a
}

Errors:

Error : the call operator is not defined for main::bar and {}
 --> main.yr:(3,28)
 3  ┃ def foo (foo_a : i32 = bar ()) -> i32 {
    ╋                            ^^
    ┃ Note : candidate bar --> main.yr:(8,5) : main::bar (bar_a : i32)-> i32
    ┃ Note : 
    ┃  --> main.yr:(3,10)
    ┃  3  ┃ def foo (foo_a : i32 = bar ()) -> i32 {
    ┃     ╋          ^^^^^
    ┃ Note : 
    ┃  --> main.yr:(8,24)
    ┃  8  ┃ def bar (bar_a : i32 = foo ()) -> i32 {
    ┃     ╋                        ^^^
    ┃ Note : 
    ┃  --> main.yr:(8,10)
    ┃  8  ┃ def bar (bar_a : i32 = foo ()) -> i32 {
    ┃     ╋          ^^^^^
    ┃ Note : 
    ┃  --> main.yr:(3,24)
    ┃  3  ┃ def foo (foo_a : i32 = bar ()) -> i32 {
    ┃     ╋                        ^^^
    ┗━━━━━┻━


This recursivity problem can be easily resolved by setting a value to the parameter bar_a when called in the default value of foo_a.

def foo (foo_a : i32 = bar (bar_a-> 20)) -> i32 {
                    //      ^^^^^ resolve the recursive problem 
    foo_a
}

// no need to do the same in bar, the recursivity does not exists anymore


Results:

Bar 20
Bar 31
Main 42

Main function parameters

The main function can have a parameter. This parameter is of type [[c8]], and is the list of arguments passed to the program in the command line when called.

import std::io;

def main (args : [[c8]]) {
    println (args);
}

Results:

$ ./a.out foo bar 1
[./a.out, foo, bar, 1]

The std provides an argument parser in std::args, that will not be presented here, but worth mentioning.

Function body

The body of a function is an expression. Every expression in Ymir are typed, but that does not mean that every expression have a value, as they can be typed as void expression. The expression (body of the function) is evaluated when the function is entered, and its value is used as the value of the function. A simple add function can be written as follows:

def add (x : i32, y : i32)-> i32 
    x + y


Or by using a more complex expression, such as scope, which is an expression containing a list of expression. A scope is surrounded by the curly brackets, and was presented in the section regarding lifetime of local variables. The last expression in the list of expression of a scope, is taken as the value of the scope.


def add (x : i32, y : i32) -> i32 { // start of a block
    x + y // last expression of the block is the value of the block
} // end of a block

def main () 
    throws &AssertError
{
    let x = {
        let y = add (1, 2);
        y + 8 
    };
    assert (x == 11)
}


The semi-colon token ; is a way of specifying that an expression ends inside a scope, and that its value must be ignored. If the last expression of a scope is terminated by a semi-colon, an empty expression is added to the scope. This empty expression has no value, giving to the scope an empty value of type void as well.


/**
 * The value of foo is '9'
 */
def foo () -> i32 
    9


def main () {
    let x = {
         foo (); // Call foo, but its value is ignored
    } // The value of the scope is 'void'
}


Because it is impossible to declare a variable with a void type, that contains no value, the above example is no accepted by the language. The compiler returns the error depicted below. One can note, that it is however possible the declare a variable without value, but its type must be an empty tuple, defined by the literal ().

Error : cannot declare var of type void
 --> main.yr:(6,9)
    | 
 6  |     let x = {
    |         ^

ymir1: fatal error: 
compilation terminated.

Function return type

When the value of the body of a function is not of type void, the function has as well a value with a type. This type must be defined in the prototype of the function, to be visible from the other function that can call it. This type declaration is made with the single arrow token -> after the declarations of the parameter of the function. The return type of a function can be omitted if the value of its body is of type void, but must be specified otherwise.

def foo (x : i32)-> i32 
    x + 1
    
def bar (x : i32, y : i32) -> i32 {
    let z = x + y;
    println ("The value of z : ", z);
    foo (z)
}


It is not always convenient to define a body of a function in a way that leads to return the right value, when many branches are possible. To avoid verbosity, and return function prematuraly, the keyword return, close a function and return the value of the expression associated with it. This return statement can also be used in a void function, if its expression is of type void. The type of the value of the expression associated to the return statement must be the same as the function return type defined in its prototype.

def isDivisable (x : i32, z : i32) -> bool {
    if (z == 0) return false; 
    
    (x % z) == 0
}


The compiler checks that every branches leads to a return statement or to a value of the right type. If a function body has a type different to the return type of the function, and it can happen that no return statement is encountered, then the compiler returns an error.

import std::io

def add_one (x : i32)-> i32 {
    x + 1; // the value of the block is void, due to the ';'
}

def main () {
    let x = add_one (5); 
    println ("The value of x : ", x);
}


In the above source code, the function add_one has a body of type void, when the function prototype claims that the function returns a i32, and no return statement can be encountered inside the function, thus the compiler returns the following error.

Error : incompatible types i32 and void
 --> main.yr:(3,29)
 3  ┃ def add_one (x : i32)-> i32 {
    ╋                             ^
    ┃ Note : 
    ┃  --> main.yr:(5,1)
    ┃  5  ┃ }
    ┃     ╋ ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Scope declaration

A scope is also the opening of a local module, in which declaration can be made. These declarations can be other functions, structures, classes, enumeration, etc. The declarations made inside a scope have no access to the local variables defined in the function. Such access is possible with the use of closures (cf. Function advanced), but this is not be presented inside this chapter.

def foo () {
    import std::io;	 // imporation is local to foo
    let x = 12;
    {
    def bar () -> i32 {
        println (x);
        12
    }
    println (x + bar ());
    }
    
    // bar is not accessible anymore
    bar (); // does not compile
}

def main () {
    foo ();
    
    bar ();
    println ("In the main function !");
}


In the above example, the bar function is available in the scope opened at line 4, until its end at line 10. For that reason, it is also not available inside the main function. Moreover, the import statement made at line 2 (importing the println function) is only available in the scope opened at line 1, and for that reason not available in the main function. For these reasons, the above example contains five errors, that are thrown by the compiler.

Error : undefined symbol x
 --> main.yr:(6,15)
 6  ┃ 	    println (x);
    ╋ 	             ^

Error : undefined symbol bar
 --> main.yr:(9,15)
 9  ┃ 	println (x + bar ());
    ╋ 	             ^^^

Error : undefined symbol bar
 --> main.yr:(13,5)
13  ┃     bar (); // does not compile
    ╋     ^^^

Error : undefined symbol bar
 --> main.yr:(19,5)
19  ┃     bar ();
    ╋     ^^^

Error : undefined symbol println
 --> main.yr:(20,5)
20  ┃     println ("In the main function !");
    ╋     ^^^^^^^


ymir1: fatal error: 
compilation terminated.


Functions are not modules, this way of defining is used to define private symbols only, in a future chapter we will see a way to define public symbols available for other functions, and foreign modules (cf. Modules).

Uniform call syntax

The uniform call syntax is a syntax that allows to call a function with the dot operator .. The uniform call syntax places the first parameter of the function at the left of the dot operation, and the rest of the arguments of the function after the right operand as a list of expressions separated by comas enclosed inside parentheses.

ufc := expression '.' expression '(' (expression (',' expression)*)? ')'

This syntax is used to perform continuous data processing and to make the source code easier to read. This syntax is named uniform call syntax because it is similar to the the syntax used to call methods on class objects (cf. Objects).

import std::io

def plusOne (i : i32) -> i32 
    i + 1

def plusTwo (i : i32) -> i32
    i + 2
    
def main () {
    let x = 12;
    x.plusOne ()
     .plusTwo ()
     .println ();	 	 
}	


Results:

15


The uniform call syntax can also be useful to define equivalent of methods on structures. Because structures are presented in a future chapter, we do not present this possibility here.

Control flows

When writing a program, the ability to decide to execute part of the code conditionally, or to repeat part of the code, is a basic scheme that is necessary.

If expression

An if expression is a control flow allowing to branch into the program code by making decisions based on conditions. An else can be placed after an if expression, to execute a part of code, if the condition of the if expression is not met. The syntax of the if expression is presented in the following code block.

if_expression := 'if' expression expression ('else' expression)?


The following source code present a basic utilization of the if expression.

def main () {
    let x = 5;
    
    if x < 5 {
       println ("X is lower than 5");
    } else if (x == 5) { // parentheses are optional
      println ("X is exactly 5");
    } else {
      println ("X is higher than 5");
    }
}


The value of an if expression is computed by the block of code that is executed when branching on the condition. Each branch of the if expression must have a value of the same type, otherwise an error is returned by the compiler. The value of an if, can of course be of type void.

def main () {
    let condition = true;
    let x = if condition {
        5 
    } else {
        7
    };
}


If there is a possibility for the program to enter none of the branch of the if expression, then the value of the whole if expression is of type void. For example, in the following source code, the variable condition can be either true or false, leading to the possibility for the if expression defined at line 5 to be never entered, and to the possibility for that the value of x to be never set.

def foo () -> bool { // ... } // return a bool value

def main () {
    let condition = foo ();
    let x = if condition { // the condition can be false
        5 
    }; // and then the expression has no value
       // but the variable x cannot be of type void
}

Errors:

Error : incompatible types void and i32
 --> main.yr:(5,10)
 5  ┃ 	let x = if condition { // the condition can be false
    ╋ 	        ^^
    ┃ Note : 
    ┃  --> main.yr:(6,3)
    ┃  6  ┃ 		5 
    ┃     ╋ 		^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Loops

In Ymir, there are three kinds of loops: loop, while and for.

Infinite repetitions

The keyword loop is used to specify that a scope must be repeated endlessly. The syntax of the loop expression is the following:

loop_expression := 'loop' expression


In the following example, the program will never exit, and will print, an infinite number of times, the string "I will be printed an infinite number of times".

def main () {
    loop { // the loop will never exit
         println ("I will be printed an infinite number of times");
    }
}


A loop can be used to repeat an action until it succeeds, e.g. waiting for the end of a thread, or waiting for incoming network connections, etc. The keyword break is used to stop a loop. A break statement is associated with a value, which is following the keyword. The value of a loop is defined by the value given by the break statement. Every break statement in a loop must share the same type. A loop can evidently be of type void.

import std::io

def main () {
    let mut counter = 0;
    
    let result = loop { 
        counter += 1;
        if counter == 10 {
            break counter + 1; // stop the loop and set its value to 'counter + 1'
        }
    };
    println ("Result : ", result);}

Results:

Result : 11


Loop while condition is met

The keyword while creates a loop, which repeats until a condition is no longer satisfied. As for the loop, it can be broken with the keyword break. Unlike loop the value of a while loop is always of type void, because it is impossible to ensure that the while is entered at all. The break statement must follow that rule, and break only with values of type void. Contribution: It is planned to add the possibility to write an else after a while loop to give a value to the while loop when it is not entered.

The grammar of the while loop is presented, in the following code block.

while_expression := 'while' expression expression


The following example, present an utilization of a while loop, where the loop iterates 10 times, while the value of i is lower than 10.

import std::io

def main () {
    let mut i = 0;
    while i < 10 {
        i += 1;	
    };
    
    println ("I is : ", i);
}


Results:

I is : 10


Iterate over a value

The last type of loop is the for loop defined with the keyword for. Like for the while loop the value of a for loop is always void as it is impossible to garantee that the loop is entered even once. The for loop iterates over an iterable type. Primitive iterable types are ranges, tuple, slices and static arrays.

for_expression := 'for' ('(' var_decls ')' | var_decls) 'in' expression expression

var_decls := var_decl (',' var_decl)
var_decl := (decorator)* identifier (':' type)?

decorator := 'ref' | 'mut' | 'dmut'


1) Iteration over a range. In the following example, the for loop is used to iterate over three ranges. The first loop at line 4, iterates between 0 and 8 (not included), by a step of 2. The second loop iterate between the value 10 and 0 (not included) with a step of -1. The third loop iterates between the value 1 and 6 (included this time).

import std::io
    
def main () {
    for i in (0 .. 8).step_by (2) {
        println (i);
    }	
    
    for i in 10 .. 0 {
        println (i);
    }
    
    for i in 1 ... 6 {
        println (i);
    }
}


2) iteration over slices and static arrays. Slices are iterable types. They can be iterated using one or two variables. When only one variable is used, it is associated with the values contained inside the slice. When two variable are used, the first variable is associated to the current iteration index, and the second variable to the values contained inside the slice. Static array iteration works the same.

import std::io;

def main () {
    let a = [10, 11, 12];
    for i in a {
        print (i, " ");
    }
    
    println ("");
    for i, j in a {
        print (i, "-> ", j, " ");
    }
    println ("");
}

Results:

10 11 12 
0-> 10 1-> 11 2-> 12


Contribution: the iteration by reference over mutable slice, and mutable static arrays is currently under development.

3) iteration over tuples. Tuple are iterable types. But unlike slice, or range the for loop is evaluated at compilation time. The tuple can be iterated using only one variable, that is associated to the values contained inside the tuple.

import std::io 

def main () {
    let x = (1, 'r');
    for i in x { 
        println (i);
    }
    
    // Is equivalent to 
    println (x.0);
    println (x.1);
}


One may note that the type of the variable i in the for loop of the above example changes from one iteration to another, being of type i32 at first iteration and then of type c32. For that reason, the for loop is not really dynamic, but flattened at compilation time. This does not change anything from a user perspective, but is worth mentioning, to avoid miscomprehension of static type system, there is no hidden dynamicity here.

Assertion

The expression assert is an expression that verify the validity of a condition and throws an exception if the condition is false. Error are presented in the chapter Error Handling, thus no detail are given in this section.

def foo (i : i32) throws &AssertError 
{
    assert (i < 10, "i must be lower than 10")
}

def main () 
    throws &AssertError
{
    foo (11);
}

Operator priority

The following table present the precedence of the operators, and literals. This table presents the priority of the operators, but does not specify how the operators are used, and their specific syntax. For example, there are unary operators, and binary operators, that require respectively one and two operands, but that is not specified in the table.

Priority Description Operators Comments
0 Assignement operators = /= -= += *= %= ~= <<= >>=
1 Logical Or ||
2 Logical And &&
3 Comparison operators < > <= >= != == of is in !of !is !in Cannot be chained
4 Range operators .. ...
5 Bitshift operators << >>
6 Bit operators | ^ & Warning there is no priority over these operators (or and and)
7 Additive operators + ~ - ~ is the concatenation operator
8 Multiplicative operators * / %
9 Power operator ^^
10 Unary operators - & * ! Always prefixed
11 Option operator ? Always postfixed
12 Keyword and Scope operators { if while assert break do for match let return fn dg loop throw __version __pragma with atomic This operators have a specific syntax that must be closed, to be completed
13 Postfix operators . ( [ :. #[ #( #{ ( [ #{ #[ #( must be closed by a balanced ], ) or } to be completed
14 Path operator ::
15 Literal operators ( ! [ | cast move In that case ( [ | start a new expression, move and | start a lambda literal, ( a tuple, or a 0 priority expression, [ a slice or array literal, ! a template call
16 Decorated expression ref const mut dmut cte
17 Anything else A variable, a literal, etc.

Alias and References

The alias and reference is one of the most important characteristics of the Ymir language, which allows it to give guarantees on the mutability of the data, and the explicit movement of the memory. It is important to understand how memory works in Ymir, in order to understand the error message you might get when you try to move data from one variable to another.

Ymir is a high level programming language, thus there is no need to worry about memory management (memory leaks), the language using a garbage collector. However, in terms of mutability and access rights, the language provides an expressive system for managing memory movements.

Standard and Aliasable types

In Ymir, there are two types, standard types and aliasable types. A value whose type is a standard type, can be copied without the need of explicitly inform the compiler. The standard types are all primitive scalar types. On the other hand, aliasable types are types that have borrowed data, which will not be copied unless it is explicitly written into the code, to avoid performance loss.

To understand how data is represented in a program, you need to know the difference between heap and stack. The stack is a space allocated by the program when a function is entered, which is released when the function is exited. On the other hand, the heap is a space that is allocated when certain instructions in the program require it, such as allocating a new slice, allocating a new object instance, and so on.

When a slice is allocated, all its data is stored in the heap, and the address of this data is stored in the stack, where the variables are located. The following figure shows the data representation for this program:

def foo () {
    let x = [1, 2, 3];
}


drawing

Mutability level

We define the level of mutability as the deepest level of the type that is mutable. An example of a mutability level is shown in the following table:

Type Level
mut [i32] 1
[i32] 0
mut [mut i32] 2
dmut [[[i32]]] 4

This is mainly used to ensure that the borrowed data is not changed by another variable in a foreign part of the program. The users have full control over the data they have created. The example below shows how the mutability level is used to ensure that the content of a table is never changed.

import std::io

def main () 
    throws &OutOfArray
{
    let mut x = [1, 2, 3];
    x = [2, 3, 4];

    x [0] = 8;
}


The type of x in the above example is mut [i32]. The mutability of the internal part of the slice (i32 value) is not specified. The compiler, for security reasons, infered it as immutable. The line 5 of the previous example is accepted, because the variable x is mutable, however, the value pointed by the slice contained in x is not. For that reason the line 9 is not accepted and the compiler returns the following error.

Error : left operand of type i32 is immutable
 --> main.yr:(7,7)
 7  ┃     x [0] = 8;
    ╋       ^


ymir1: fatal error: 
compilation terminated.


If the mutability level defines the write permission of every data, it is assumed that every parts of the code that have access to a give value have read permission on it. For that reason, in the previous example, even if writting into x [0] is not permitted, reading its value is allowed.

Deep mutability

Earlier we introduced the keyword dmut, this keyword is used to avoid a very verbose type statement, and defines that every subtype are mutable. This keyword is applicable to all types, but will only have a different effect from the mut decorator on aliasable types. The following table gives an example of an slice type, using the keyword dmut :

Type Verbose equivalent
dmut [i32] mut [mut i32]
dmut [[[i32]]] mut [mut [mut [mut i32]]]

If we come back to our previous example, and change the type of the variable x, and use the keyword dmut. The variable x now borrows mutable datas, that can be modified, thus the expression at line 9 is accepted.

import std::io

def main () 
    throws &OutOfArray
{
    let dmut x = [1, 2, 3];
    x = [2, 3, 4];
    
    x [0] = 8;
}

Const keyword

The const keyword is the perfect opposite of the dmut keyword. This keyword has no interest when defining types directly (because they are immutable by default), but coupled with the keyword typeof, it can transform a mutable type into a immutable type.

import std::io;

def main () {
    let mut x = 12;
    println (typeof (x)::typeid);
    println ((const typeof (x))::typeid);
}

Results:

mut i32
i32

String literal

Strings literal, unlike slice literals, are in the text segment of the program (read-only part of a program). This means that the type of a literal string is [c32] (or [c8] if the suffix s8 is specified), while the type of a literal array (of i32 for example) is mut [mut i32]. For that reason, it impossible to borrow the data into a deeply mutable variable.

import std::io

def main () {
    let dmut x = "Try to make me mutable !?";
}


The compiler returns an error. This error means that the mutability level of the right operand is 1, here mut [c32], (the reference of the array is mutable but not its content), and the code try to put the reference inside a variable of mutability level 2, that is to say of type mut [mut c32]. If this was allowed the variable x would have the possibility to change data that has been marked as immutable at some point of the program, so the compiler does not allow it, and returns the following error.

Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
 --> main.yr:(4,11)
 4  ┃ 	let dmut x = "Try to make me mutable !?";
    ╋ 	         ^
    ┃ Note : 
    ┃  --> main.yr:(4,15)
    ┃  4  ┃ 	let dmut x = "Try to make me mutable !?";
    ┃     ╋ 	             ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Memory borrowing

When you want to make a copy of a value whose type is aliasable, you must tell the compiler how you want to make the copy. There are four ways to move or reference memory, which are provided with the four keywords ref, alias, copy and dcopy. The following chapters presents these keywords, and the semantic associated to them.

Reference

The keyword**ref** is a keyword that is placed before the declaration of a variable. It is used to refer to a value, which is usually borrowed from another variable. They are performing similar operation as Pointers, with the difference that they does not need to be dereferenced (this is done automatically), and pointer arithmetics is not possible with references. In Ymir references are always set, and are always set from another variable, hence they are way safer than pointers, and must be prefered to them when possible.

def foo () {
    let x = [1, 2, 3];
    let ref y = ref x;
    //          ^^^    
    // Try to remove the keyword ref.
}


The above program can be represented in memory as shown in the following figure.


<img src="https://gnu-ymir.github.io/Documentations/en/advanced/memory_x__ref_y_foo.png" alt="drawing" height="500", style="display: block; margin-left: auto; margin-right: auto;">


In this figure, one can note that y, is a pointer to x, which can be used as if it was directly x. This means that y must have the same mutability properties (or lower) as x. And that if x is mutable, changing the value of y would also change x.

A first example of reference is presented in the following source code. In this example, a mutable variable x contains a value of type i32. This value is placed on the stack, as it is not a aliasable type. Then a variable y is constructed as a reference of the variable x. Modifying y in the following example, also modifies x.

def main ()
    throws &AssertError
{
    let mut x = 12; // place a value of type i32 and value 12 on the stack
    let ref mut y = ref x; // create a reference of x
    y = 42; // modify the value pointed by the reference
    assert (x == 42);
}


A more complexe example is presented in the following source code. In this example, a deeply mutable array x is created. This array is a reference on borrowed data in the heap. A deeply mutable reference y is the, made on that variable x, which is allowed because x is also deeply mutable and the mutability level of x and y are the same. When changing the value of y (here the reference of the slice), it does not only change the reference of y but also the reference of x.

def main () {
    let mut x : [mut i32] = [1, 2, 3];
    let ref mut y : [mut i32] = ref x;
    y = [7, 8, 9]; // modify the value pointed by the reference (in the stack)
    y [0] = 89; // modify the value on the heap
    assert (x == [89, 8, 9]); 
}

Reference as function parameter

A parameter of a function can be a reference. As with the local variable, when a value is passed to it, you must tell the compiler that you understand that you are passing the value by reference, and accept the side effects it may have on your values.

import std::io

def foo (ref mut x : i32) {
    x = 123;
}

def main () {
    let mut x = 12;
    //  ^^^
    // Try to remove the mut
    foo (ref x);
    //   ^^^
    // Try to remove the ref
    println (x); 
}


The following figure shows the memory status of the previous code:

drawing

The keyword ref is not always associated with a mutable variable, it can be used to pass a complex type to a function more efficiently, when you don't want to make a complete copy, which would be much less efficient. In this case, you should always specify that you pass the variable by reference, to distinguish it from the function that passes the variable directly by value. In practice, due to the existence of aliasable types, which will be discussed in the next chapter, you will never gain anything by doing this.

import std::io

def foo ( x : i32) {
//       ^
// Try to add mut here
    println ("By value : ", x);
}

def foo (ref x : i32) {
    println ("By reference : ", x);
}

def main () {
    let x = 89;
    foo (x);
    foo (ref x);
}

Results:

By value : 89
By reference : 89


If you have done the exercise, and added the keyword mut to the signature of the first function foo, you should get the following error:

Error : a parameter cannot be mutable, if it is not a reference
 --> main.yr:(3,15)
 3  ┃ def foo (mut  x : i32) {
    ╋               ^


This error means that the type of x is not aliasable, so if it is not a reference, marking it as mutable will have no effect on the program, so the compiler does not allow it.

Reference as a value

A reference is not a type, it is only a kind of variable, you cannot store references in subtypes (for example, you cannot make an array of references, or a tuple containing a reference to a value). This means that with the following code, you should will get an error.

def main () {
    let x = 12;
    let y = (10, ref x);
}


The following error means that the source code intended to create a reference on a variable, but the compiler will not make it, as it has no interest and will be immediately dereferenced to be stored in the tuple value.

Warning : the creation of ref has no effect on the left operand
 --> main.yr:(3,22)
 3  ┃     let y = (10, ref x);
    ╋                      ^


ymir1: fatal error: 
compilation terminated.

Reference as function return

You may be skeptical about the interest of returning a reference to a variable, and we agree with you. That is why, it is impossible to return a reference to a variable as a function return value.

import std::io

def foo () -> ref i32 {
    let x = 12;
    ref x
}

def main () {
    let ref y = ref foo (); // x would no longer exists
    println (y); // and a seg fault would be raised, when using the reference
}


With the above source code, the compiler return this fairly straightforward error.

Error : cannot return a reference type
 --> main.yr:(3,19)
 3  ┃ def foo () -> ref i32 {
    ╋                   ^^^

Alias

All types that containing a pointer to data in the heap (or the stack) are aliasable types. An aliasable type cannot be implicitly copied, nor can it be implicitly referenced, for performance and security reasons respectively. There are mainly three aliasable types, arrays (or slices, there is no difference in Ymir), pointers, and objects. Structures and tuples containing aliasable types are also aliasable.

The keyword alias is used to inform the compiler that the used understand that the data borrowed by a variable (or a value) will borrowed by another values.

import std::io

def main () {
    let mut x : [mut i32] = [1, 2, 3];
    let mut y : [mut i32] = alias x; // allow y to borrow the value of x
    //                      ^^^^^
    // Try to remove the alias
    println (y);
}


This source code can be represented in memory by the following figure.

drawing

The alias keyword is only mandatory when the variable that will borrow the data is mutable and may impact the value. It is obvious that one cannot borrow immutable data from a variable that is mutable. For example, the compiler must return an error on the following code.

import std::io

def main () {
    let x = [1, 2, 3];
    let mut y : [mut i32] = alias x; // try to borrow immutable data in deeply mutable variable y  
    //                      ^^^^^
    // Try to remove the alias
    println (y);
}


Errors:

Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
 --> main.yr:(5,13)
 5  ┃     let mut y : [mut i32] = alias x; // try to borrow immutable data in deeply mutable variable y  
    ╋             ^
    ┃ Note : 
    ┃  --> main.yr:(5,29)
    ┃  5  ┃     let mut y : [mut i32] = alias x; // try to borrow immutable data in deeply mutable variable y  
    ┃     ╋                             ^^^^^
    ┗━━━━━┻━ 

Error : undefined symbol y
 --> main.yr:(8,14)
 8  ┃     println (y);
    ╋              ^


ymir1: fatal error: 
compilation terminated.


However, if the variable that will borrow the data is not mutable, there is no need to add the keyword alias, and the compiler will create an implicit alias, which will have no consequences.

import std::io

def main () {
    let x = [1, 2, 3];
    let y = x; // implicit alias is allowed, 'y' is immutable
    println (y);
}


In the last example, y can be mutable, as long as its internal values are immutable, i.e. its type is mut [i32], you can change the value of y, but not the values it borrows. There is no problem, the values of x will not be changed, no matter what is done with y.

import std::io

def main () {
    let x = [1, 2, 3];
    let mut y = x; 
    // y [0] = 9;
    // Try to add the above line 
    
    y = [7, 8, 9];
    println (y);
}


You may have noticed that even though the literal is actually the element that creates the data, we do not consider it to be the owner of the data, so the keyword alias is implied when it is literal. We consider the data to have an owner only once it has been assigned to a variable.

There are other kinds of alias that are implicitly allowed, such as code blocks or function calls. Those are implicit because the alias is already made within the value of these elements.

import std::io

def foo () -> dmut [i32] {
    let dmut x = [1, 2, 3];
    alias x // alias is done here and mandatory
}

def main ()
    throws &AssertError
{
    let x = foo (); // no need to alias, it must have been done in the function
    assert (x == [1, 2, 3]);
}


Alias a function parameter

As you have noticed, the keyword alias, unlike the keyword ref, does not characterize a variable. The type of a variable will indicate whether the type should be passed by alias or not, so there is no change in the definition of the function. When the type of a parameter is an aliasable type, this parameter can be mutable without being a reference.

import std::io

// The function foo will be allowed to modify the internal values of y
def foo (mut y : [mut i32])
    throws &OutOfArray
{
    y [0] = y [1];
    y = [8, 3, 4]; // has no impact on the x of main,
    // y is a reference to the data borrowed not to the variable x itself
}

def main ()
    throws &OutOfArray, &AssertError
{
    let dmut x = [1, 2, 3];
    foo (alias x);
    //   ^^^^^
    // Try to remove the alias
    assert (x == [2, 2, 3]);
}


As with the variable, if the function parameter cannot affect the values that are borrowed, the alias keyword is not required.

import std::io

def foo (x : [i32]) {
    println (x); // just reads the borrowed data, but doesn't modify them
}

def main () {
    let dmut x = [1, 2, 3];	
    foo (x); // no need to alias
}


Alias in uniform call syntax

We have seen in the function chapter, the uniform call syntax. This syntax is used to call a function using the dot operator ., by putting the first parameter of the function on the left of the operation. When the first parameter is of an aliasable type, the first argument must be aliased explicitely, leading to a strange and verbose syntax.

let dmut a = [1, 2, 3];
(alias a).foo (12); // same a foo (alias a, 12);


To avoid verbosity, we added the operator :., to use the uniform call syntax with an aliasable first parameter.

let dmut a = [1, 2, 3];
a:.foo (12); // same as foo (alias a, 12);


This operator is very usefull when dealing with classes, where the uniform call syntax is mandatory, as we will see in chapter Class.

Special case of struct and tuple

In the chapter Structure you will learn how to create a structure containing several fields of different types. You have already learned how to make tuples. These types are sometimes aliasable, depending on the internal type they contain. If a tuple, or a structure, has a field whose type is aliasable, then the tuple or structure is also aliasable.

The table below presents some examples of aliasable tuples :

Type Aliasable Reason
(i32, i32) false i32 is not aliasable
([i32],) true [i32] is a slice, and hence aliasable
([i32], f64) true [i32] is a slice, and hence aliasable
(([i32], i32), f64) true [i32] is a slice, and hence aliasable

In the introduction of this chapter we presented the notion of Mutability level. One can note that mutability level is not suitable for tuple, as aliasable tuple are trees of type and not simply a list. However, this does not change much, the compiler just check the mutability level of the inner types of the tuple, recursively.

Copy data to make them mutable

Sometimes it is not possible to allow data to be borrowed by foreign functions or variables. This can be due to the facts that data are immutable for example. To solve this problem, Ymir provides two keywords, copy and dcopy.

Copy

The copy keyword makes a copy of the first level of a value, whose type is aliasable. This copy transform an immutable type into a mutable one, by increasing its mutability level by one. The following table shows some examples of the types of copied values :

Type Type of copied value
[i32] mut [mut i32]
mut [i32] mut [mut i32]
mut [[i32]] mut [mut [i32]]

An example of what can be achieved by copy keyword is shown in the following code. The representation of the memory is also shown in the figure underneath. In this example, the variable x is copied and the result value is placed in the variable y. In this example, each variable are borrowing different data placed on the heap, whose values are equivalent.

import std::io
    
def main ()
    throws &AssertError, &OutOfArray
{
    let x = [1, 2, 3];
    let dmut y = copy x; // create a copy of x
    assert (x == y); // y and x have the same value, but at different location

    y [0] = 9; 
    assert (x == [1, 2, 3]); // modifying y does not affect x
    assert (y == [9, 2, 3]); // but still affects y
}


We can see from the figure below, that the variable y points to data at a different location, from the data pointed by x. This implies a new memory allocation, and a memory copy, that cost some cpu time, and memory place. For that reason, copies are never hidden by the language, and are made only when the keyword copy is placed in the source code.

drawing

Exercise : Modify x that is initialised with an imutable string literal :

import std::io

def main () 
    throws &OutOfArray 
{
    let x = "hello !";
    x [0] = 'H'; // Make this line work
    assert (x == "Hello !");
}
Correction (spoiler) :
{%s%}
import std::io

def main () throws &OutOfArray, &AssertError { let dmut x = copy "hello !"; x [0] = 'H'; // Well done assert (x == "Hello !"); }

{%ends%}

Deep copy

The deep copy will make a copy of the value and all internal values, it must be used in special cases because it is much less efficient than the simple copy, which copies only one level of the data. There is nothing complex to understand in deep copy, it simply creates a value, deeply mutable, which is an exact copy.

import std::io

def main () {
    let x = [[1], [2, 3], [4]];
    let dmut y = dcopy x;
    let mut z : [mut [i32]] = copy x;
    println (x, " ", y, " ", z);
}


The structure of the copy respect the structure of the initial value that has been copied, meaning that even recursive values can be copied without any worries. To make recursive values, we need to use objects, that are described in the chapter Objects, and traits described in the chapter Traits to make the objects deeply copiable. To avoid the scattering of the information, we will assume that you will have already read these chapters and came back here to understand the deep copy on objects.

In the following example, the object A contains a field of type A. The initialization of this field is made using the either self or another object in the constructor defined at line 1 and 2. Thus the state of the memory in the main function, at line 1 can be described by the figure depicted just underneath the source code.

import std::io;

class A {
    
    let _i : i32;
    let dmut _a : &A;
    
    pub self (i : i32, dmut a : &A) with _a = alias a, _i = i {
        self._a._a = alias self;
    }
    
    pub self (i : i32) with _a = alias self, _i = i {}
    
    impl Copiable, Streamable; // to make A deep copiable, and printable
}

def main () {
    let dmut a = A::new (1);
    let dmut b = A::new (2, alias a);
    
    println (a);
    println (b);
}


Results:

main::A(1, main::A(2, main::A(...)))
main::A(2, main::A(1, main::A(...)))


Memory state at line 20 :

drawing

Now let's add a deep copy of the value contained inside the variable a into a variable c. This deep copy copies the values of the object inside a, and the object inside the field _a, the copy is recursive, and correctly keeps the structure.

let c = dcopy a;


The following figure represents the memory state of the program after the deep copy.

drawing

Best practice

The copy is never hidden in the source code that is available to the user. However, many codes that we are using on a daily basis, are provided by libraries. In libraries, only the prototypes of the functions are presented to the user, and therefore if a copy is made inside a function, the copy is hidden from the user. We cannot guarantee that such copy are not made (at least for the moment), so we propose a best practice advice to avoid hidden copies inside libraries.

This advice is simple, never take a immutable parameter in a function, if you have to make a copy of it inside the function. For example, let say we have a function that sorts a slice. This function should preferably take a mutable slice as input and modify it directly.

def good (dmut slc : [i32])-> dmut [i32] {
    // perform the sort on slc
    alias slc
}

def bad (slc : [i32])-> dmut [i32] {
    let dmut res = copy slc;
    // perform the sort on res
    alias res}
    


This way, the function calling the sort function has the choice of making the copy or not. In the following example, the user has the choice when calling the function good, but never when calling the function bad, making the copy hidden. One can note from the following example, that the copy is never hidden when calling good, and that it is also possible to make no copy at all.

def main () {
    let dmut slc = [9, 3, 7];
    let dmut aux = good (copy slc); // slc is unchanged, and aux is sorted
    let dmut slc2 = good (alias slc); // slc is sorted, and slc2 points the data of slc
    
    good (slc); // impossible, implicit alias is not allowed
    
    bad (slc); // here there is not need for alias, nor copy, 
               // the data of slc won't be modified in bad
               // the copy is alway made and hidden
}


From the above example, the compiler returns an error, when trying to call the function good without aliasing nor copying at line 6. This error prevents from copying values implicitely without writting it down, nor making aliasing of the values and giving the write permission to foreign functions without informing the compiler of our agreement. All the other calls are valid, the wish of the user being totally explicit.

Error : the call operator is not defined for main::good and {mut [mut i32]}
 --> main.yr:(17,7)
17  ┃ 	good (slc); // impossible, implicit alias is not allowed
    ╋ 	     ^   ^
    ┃ Note : candidate good --> main.yr:(1,5) : main::good (slc : mut [mut i32])-> mut [mut i32]
    ┃     ┃ Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
    ┃     ┃  --> main.yr:(17,8)
    ┃     ┃ 17  ┃ 	good (slc); // impossible, implicit alias is not allowed
    ┃     ┃     ╋ 	      ^^^
    ┃     ┃     ┃ Note : implicit alias of type mut [mut i32] is not allowed, it will implicitly discard constant qualifier
    ┃     ┃     ┃  --> main.yr:(17,8)
    ┃     ┃     ┃ 17  ┃ 	good (slc); // impossible, implicit alias is not allowed
    ┃     ┃     ┃     ╋ 	      ^^^
    ┃     ┃     ┗━━━━━┻━ 
    ┃     ┃ Note : for parameter slc --> main.yr:(1,16) of main::good (slc : mut [mut i32])-> mut [mut i32]
    ┃     ┗━━━━━━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Contribution: Maybe it is possible to complitely avoid hidden copies ? (I don't have any clue for the moment).

Pure values

A pure value is a value that cannot change. In other word it is a value, that ensure that there is no variable in the program that has a mutable access to it. Pure values are different to const values, in the sense that const values just give the guarantee that the current access is not mutable, however they does not guarantee that there is no other variable in the program with mutable access.

The different guarantees on values can be listed as follows :

  1. No guarantee, the value is mutable mut
  2. Guarantee that the value is no writable in the current context const
  3. Guarantee that the value is completely immutable in every context pure

Limitation of const values

To understand the limitation of const values, the following program has two variable defined in the function main. The first one a has a mutable access to the value, and the variable b has a const access to the same value. Because the variable a modifies the value, the value pointed by b is also modified even if it was const. Thus, it is important to understand that const is only referering to the permission of the variable b.

import std::io;

def main () {
    let dmut a = [1, 2, 3];
    let b = a;
    
    a [0] = 98;
    
    println (b);
}


Results:

[98, 2, 3]


We have seen in a previous chapter that the keywords copy and dcopy can be used to remove this limitation, and ensure that the value of b is the same as the value of a, but that modifying the value of a does not modify the value of b. This mechanism is the base of the one provided by pure values.

import std::io;

def main () 
    throws &OutOfArray
{
    let dmut a = [1, 2, 3];
    let b = dcopy a;
    
    a [0] = 98;
    
    println (b);
}


Results:

[1, 2, 3]


Now let's add a third variable to the equation, and let's name it c. In this variable we want to store the value of b, and ensure that the variable is never modified. If the initialization of the variable b is made in an obscure way (for example in a function that is not readable, here the foo function), then the only way to ensure that the value of c is never modified, is to make a copy of it inside the value of c. If the memory movement are easy in the following example, it may not be the case in complex program with many variable and memory movement.

import std::io;

def foo (a : [i32])-> [i32] {
    a
}

def main () 
    throws &OutOfArray
{
    let dmut a = [1, 2, 3];
    let b = foo (a);
    
    let c = dcopy b;
    let d = b;
    
    a [0] = 98;
    
    println (c);
    println (d);
}


Results:

[1, 2, 3]
[98, 2, 3]

Purity and the pure keyword


In the above example, both variable b and d have no guarantees, and there values are indeed modified. The pure keyword can be added to their definitions. In that case, the compiler checks the initialization of the values that are used, and ensure that they came from a deep copy, or another pure value.

def foo (a : [i32]) -> [i32] {
    a
}

def main () 
    throws &OutOfArray 
{
    let dmut a = [1, 2, 3];
    
    let pure b = foo (a);
}


Error : discard the constant qualifier is prohibited
 --> main.yr:(10,11)
10  ┃ 	let pure b = foo (a);
    ╋ 	         ^
    ┃ Note : implicit pure of type [i32] is not allowed, it will implicitly discard constant qualifier
    ┃  --> main.yr:(10,19)
    ┃ 10  ┃ 	let pure b = foo (a);
    ┃     ╋ 	                 ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


To avoid the above error, there is two possibilities. Either we add a dcopy on the function call of foo, or we add pure to the return type of foo and make a deep copy of the value of a in this function. The function bar of the following example perform the second possibility.

import std::io;

def foo (a : [i32])-> [i32] {
    a
}

def bar (a : [i32])-> pure [i32] {
    return dcopy a;
}

def main () 
    throws &OutOfArray 
{
    let dmut a = [1, 2, 3];
    let pure b = dcopy foo (a);
    
    let pure c = bar (a);
    
    let pure d = b;
    let pure e = c;
    
    a [0] = 98;
    
    println (d);
    println (e);
}	


Results:

[1, 2, 3]
[1, 2, 3]


One can note that not copy are needed to move the value contained in the variable c into the variable e. This is due to the fact that c is pure, so the guarantee of purity is already made. Thanks to the pure mechanism some copies can be avoided.

Modules

When creating a large project, it is very important to organize your code. Ymir offers a system of modules, which is used to manage different parts of the code that have different purposes. Each source file in Ymir is a module.

File hierarchy

Lets have a look at the following file hierarchy :

.
├── main.yr
└── extern_modules
    ├── bar.yr
    └── foo.yr

1 directory, 3 files


In this file hierarchy there are three files, which contain modules, the first module in the file main.yr will be named main. The second one in the extern_modules/bar.yr file will be named extern_modules::bar, and the third one in the extern_modules/foo.yr file will be named extern_modules::foo.

To be properly importable, the module must be defined from the relative path of the compilation, i.e. if the file is located in $(pwd)/relative/path/to/file, its module name must be relative::path::to::file.

The name of a module is defined by the first line of the source code, by keyword mod. If this line is not given by the user, the path of the module will only be the file name, so you will not always be able to import the module, depending on its relative path. You can consider this line mandatory for the moment.

For example, in the file foo.yr, the first line must look like :

mod extern_modules::foo


And, it will therefore be importable everywhere, for example in the main module, when writing the import declaration :

import extern_modules::foo


The syntax of the import statement is the following :

import_statement := 'import' path (',' path)* (';')?
path := Identifier ('::' Identifier)* ('::' '_')?

Sub modules

Sub modules are local modules, declared inside a global modules, are inside another sub module. Unlike global module, the access to the symbols defined inside them is not implicit. For that reason they have to be explicitely mentionned when trying to access to their symbols. This mention is done with the double colon binary operator ::, where the first operand is the name of the module, and the second the name of the symbol to access.

mod main
import std::io;

mod InnerModule {

    pub def foo () {
        println ("Foo");
    }

}

def main () {
    InnerModule::foo (); // access of the function declared in InnerModule
}

The access operator ::, can also be used to access to symbols declared inside global modules. This will be discussed after talking about privacy of symbols.

Privacy

All symbols defined in a module are private by default. The privacy of a given symbol s refer to the possibility for foreign modules, and symbols to access to this given symbol s. When a symbol s is declared private in a module s, then only the other symbols of the module m have access to it. Module privacy can be seen as a tree, where a global module is a root, and module symbols are the branches and leaves of the tree. In such a tree, symbols have access to their parent, siblings, and the siblings of their parents.

In the following figure an example of a module tree is presented, where a global module named A, has three symbols, 2 sub modules A::X and A::Y, and a function A::foo. In this tree, we assume that every symbols are declared private. For that reason, the function A::foo has access to A, A::X, A::Y, but not to A::X::bar, nor A::Y::baz. The symbol A::X::bar, has access to every symbols (A, A::X, A::Y, A::foo), except A::Y::baz.


drawing


Global modules are always tree roots, for that reason they don't have parents. For example, the module extern_modules::foo, does not have access to the symbols declared inside the module extern_modules, (if they are privates).

The keyword pub flag a symbol as public, and accessible by foreign modules. This keyword can be used as a block, or for only one symbol. Its syntax grammar is presented in the following code block.

pub :=   'pub' '{' symbol* '}' 
       | 'pub' symbol	

Example

  1. Module extern_modules/foo.yr
mod extern_modules::foo;

/**
 * foo is public, it can be accessed from foreign modules
 */
pub def foo () {}

/**
 * The bar function is private by default
 * Thus only usable in this module
 */
def bar () {}


  1. Module main.yr
/**
 * This importation will give access to all the symbols in the module
 * 'extern_modules::foo' that have been declared 'public'
 */
import extern_modules::foo

def main () {
    foo (); // foo is public we can call it
    bar (); // however, bar is private thus not accessible
}

Errors:

Error : undefined symbol bar
 --> main.yr:(7,5)
 7  ┃     bar (); // however, bar is private thus not accessible
    ╋     ^^^
    ┃ Note : bar --> extern_modules/foo.yr:(8,5) : extern_modules::foo::bar is private within this context
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.


Symbol conflict resolution

When two external global modules declare two symbols with the same name, it may be impossible to know which symbol the user is refereing to. In this case, the double colon operator :: can be used with the name of the module declaring the symbol to resolve the ambiguity. To give an example of symbol conflict, let's say that we have two module extern_modules::foo and extern_modules::bar declaring a function with the same signature foo.

  1. Module extern_modules/bar.yr
mod extern_modules::bar
import std::io

pub def foo () {
    println ("Bar");
}


  1. Module extern_modules/foo.yr
mod extern_modules::foo
import std::io

pub def foo () {
    println ("Foo");
}


In the main module, both modules extern_modules::bar and extern_modules::foo, are imported. The main function presented below refers to the symbol foo. In that case, there is no way to tell which function will be used, extern_modules::foo::foo or extern_modules::bar::foo. The compiler returns an error. One can note that this errors occurs only because the signature of the two function foo are the same (taking no parameters), and they are both public. If there was a difference in their prototypes, for example if the function in the module extern_modules::bar would take a value of type i32 as parameter, the conflict would be resolved by itself, as the call expression will be different.

  1. Module main.yr
import extern_modules::bar, extern_modules::foo

def main () {
    foo ();
}

Errors:

Error : {extern_modules::bar::foo ()-> void, extern_modules::foo::foo ()-> void, mod extern_modules::foo} x 3 called with {} work with both
 --> main.yr:(4,6)
 4  ┃ 	foo ();
    ╋ 	    ^
    ┃ Note : candidate foo --> extern_modules/bar.yr:(4,9) : extern_modules::bar::foo ()-> void
    ┃ Note : candidate foo --> extern_modules/foo.yr:(4,9) : extern_modules::foo::foo ()-> void
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.


.page-inner { width: 95%; } In the above error, we can see that three modules are presented. The two functions foo — in extern_modules::bar, and extern_modules::foo — and the extern_modules::foo module itself. Obviously, it is not possible to use the call operator () on a module, that is why it is not presented as a possible canditate in the notes of the error.

The conflict problem can be resolved by changing the calling expression, and using the double colon operator ::. In the following example, the full name of the module is used. This is not always necessary, as bar::foo is sufficient to refer to extern_modules::bar::foo, and foo::foo for function foo in extern_modules::foo.

import extern_modules::bar, extern_modules::foo

def main () {
    extern_modules::bar::foo (); 
    extern_modules::foo::foo (); 
    
    foo::foo (); 
    bar::foo ();
}

Results:

Bar
Foo
Foo
Bar


Public importation

As for all declaration, importation are private. It means that the importation is not recursive. For example, if the module extern_modules::foo imports the module extern_modules::bar, and the module main import the module extern_modules::foo, all the public symbols declared in extern_modules::bar will not be accessible in the module main.

You can of course, make a pub importation, to make the symbols of the module extern_modules::bar visible for the module main.

  1. Module extern_modules/bar.yr
mod extern_modules::bar
import std::io

pub def bar () {
    println ("Bar");
}


  1. Module extern_modules/foo.yr
mod extern_modules::foo

pub import extern_modules::bar


  1. Module main.yr
mod main
import extern_modules::foo;

def main () {
    bar ();
}


In the example above, the function bar defined in the module extern_modules::bar, is imported (because the function is public is public) by the module extern_modules::foo. This importation is public, thus when the module main imports the module extern_modules::foo, it also imports the module extern_modules::bar, and has access to the function bar.

Best practice

Public importation must be used with caution, to avoid polluting other modules. A good practice, is to define some modules only to make public importations. These modules should be named _. For example, with our previous file hierarchy, a file extern_modules/_.yr would be added, and no public imports made in the modules extern_modules::foo, nor in the module extern_modules::bar.

mod extern_modules::_;

pub import extern_modules::foo;
pub import extern_modules::bar;


These modules are not automatically generated by Ymir — even if it seems trivial —, to allow importing only a subset of the modules contained in a sub directory. These importation modules are optional and left to the choice of the user.

Include directory

You can use the -I option, to add a path to the include directory. This path will be used as if it was the current $(pwd). In other words, if you add the I -path/to/modules option, and you have a file in path/to/modules/relative/to/my/file, the name of the module must be relative::to::my::file.

gyc -I ~/libs/ main.yr

This is how the standard library is included in the build, and how you can access modules in std:: that are not located in $(pwd)/std/.

Compilation of modules

All modules must be compiled, the import declaration is just a directive of for symbols access, but does not compile the imported symbols. For example, in the following example, there are two modules, one declaring a function foo, and the other importing it and calling it.

  1. Module main.yr
mod main
import extern_modules::foo;

def main () {
    foo ();
}


  1. Module extern_modules/foo.yr
mod extern_modules::foo

pub def foo () {}


By compiling only the main function, the compiler returns a link error. This error means that the symbol foo declared in the module extern_modules::foo was not found during the symbol linkage.

$ gyc main.yr
/tmp/ccCOeXDq.o: In function `_Y4mainFZv':
main.yr:(.text+0x3e): undefined reference to `_Y14extern_modules3foo3fooFZv'
collect2: error: ld returned 1 exit status


To avoid this error, and create a valid executable, where all symbols can be found, the module extern_modules::foo has to be compiled as well. GYC is able to manage object files (containing pre compiled symbols), and compiled libraries. The way GYC manage these kind of objects is similar to all compiler of the GCC suite, and is not presented in this documentation (cf. GCC options for linking).

$ gyc main.yr extern_modules/foo.yr

User defined types

There are four different custom types:

  • Structure
  • Enumeration
  • Aka
  • Class

The following chapters present the structure, enumeration and aka.

Structure

Structure is a common design used in many languages to define users' custom types. They contains multiple values of different types, accessible by identifiers. Structures are similar to tuples, in terms of memory management (located in the stack). Unlike tuples, structures are named, and all their internal fields are named as well.

The complete grammar of structure definition is presented in the following code block. One can note the possibility to add templates to the definition of the structure. These templates will only be discussed in the chapter Templates, and are not of interest to us at the moment.

struct_type := 'struct' ('|' var_decl)* '->' identifier (templates)?
var_decl := ('mut'?) identifier ':' type ('=' expression)?
identifier := ('_')* [A-z] ([A-z0-9_])*	 


The fields of the structure are defined using the same syntax as the declaration of function parameters, i.e. the same syntax as variable declaration but with the keyword let omitted. The following source code presents a definition of a structure Point with two fields x and y of type i32. The two fields of this structure are immutable, and have no default values.

import std::io

struct 
| x : i32
| y : i32 
 -> Point;
 
def main () {
    let point = Point (1, 2); // initialize the value of the structure
    println (point); // structures are printable
} 


Results:

main::Point(1, 2)


It is possible to declare a structure with no fields. Note, however, that such structure has a size of 1 byte in memory.

Contribution this is a limitation observed in gcc, maybe this can be corrected ?

import std::io;

struct -> Unit;

def main () {
    let x = Unit ();
    println (x, " of size ", sizeof (x));
}

Results:

main::Unit() of size 1

Structure construction

The construction of a structure is made using the same syntax as a function call, that is to say using its identifier and a list of parameters inside parentheses and separated by comas. Like function calls, structure can have default values assigneted to fields. The value of these fields can be changed using the named expression syntax, which is constructed with the arrow operator ->. Field without default value can also be constructed using the named expression syntax. In that case, the order of field construction is not important.

import std::io

struct 
| x : i32 = 0
| y : i32 
 -> Point;
  
def main () {
    let point = Point (y-> 12, x-> 98);
    println (point);

    let point2 = Point (1);
    println (point2);
}

Results:

main::Point(98, 12)
main::Point(0, 1)


Field access

The fields of a structure are always public, and accessible using the dot binary operator ., where the left operand is a value whose type is a structure, and the right operand is the identifier of the field.

import std::io

struct 
| x : i32
| y : i32 
 -> Point;
 
def main ()
    throws &AssertError
{
    let point = Point (1, 2); 
    assert (point.x == 1 && point.y == 2);
}

Structure mutability

The mutability of a field of a structure is defined in the structure declaration. As with any variable declaration, the fields of a structure are by default immutable. By adding the keyword mut before the identifier of a field, the field becomes mutable. However, the mutability is transitive in Ymir, meaning that a immutable value of a struct type, cannot be modified even if its field are marked mutable. Consequently, for a field to be really mutable, it must be marked as such, and be a field of a mutable value.

import std::io

struct 
| x : i32
| mut y : i32
 -> Point;
 
def main () {
    let mut p1 = Point (1, 2);
    p1.y = 98; // y is mutable
                  // and p1 is mutable no problem
    
    p1.x = 34; // x is not mutable, this won't compile
    
    let p2 = Point (1, 2);
    p2.y = 98; // p2 is not mutable, this won't compile	
}

Errors:

Error : left operand of type i32 is immutable
 --> main.yr:(13,4)
13  ┃ 	p1.x = 34; // x is not mutable, this won't compile
    ╋ 	  ^

Error : left operand of type i32 is immutable
 --> main.yr:(16,4)
16  ┃ 	p2.y = 98; // p2 is not mutable, this won't compile	
    ╋ 	  ^


ymir1: fatal error: 
compilation terminated.

Memory borrowing of structure

By default structure data are located in the value that contains them, i.e. in the stack inside a variable, on the heap inside a slice, etc. They are copied by value, at assignement or function call. This copy is static, and does not require allocation, so it is allowed implicitely.

import std::io

struct 
| mut x : i32
| mut y : i32
 -> Point;

def main ()
    throws &AssertError
{
    let p = Point (1, 2);
    let mut p2 = p; // make a copy of the structure
    p2.y = 12;

    assert (p.y == 2);
    assert (p2.y == 12);
}


Structure may contain aliasable values, such as slice. In that case, the copy is no longer allowed implicitely (if the structure is mutable, and the field containing the aliasable value is also mutable, and the element that will borrow the data is also mutable). To resolve the problem, the keywords dcopy, and alias presented in Aliases and References can be used.

import std::io

struct 
| mut y : [mut [mut i32]]
 -> Point;

def main ()
    throws &OutOfArray
{
    let mut a = Point ([[1, 23, 3], [4, 5, 6]]);
    let mut b = dcopy a;
    let mut c = alias a;
    
    b.y [0][0] = 9; // only change the value of 'b'
    c.y [0][1] = 2; // change the value of 'a' and 'c'
    
    println (a);
    println (b);
    println (c);
}


Results:

main::Point([[1, 2, 3], [4, 5, 6]])
main::Point([[9, 23, 3], [4, 5, 6]])
main::Point([[1, 2, 3], [4, 5, 6]])


It is impossible to make a simple copy of a structure with the keyword copy, the mutability level being set once and for all in the structure definition. For example, if a structure S contains a field whose type is mut [mut [i32]], every value of type S have a field of type mut [mut [i32]]. For that reason, by making a first level copy, the mutability level would not be respected.

Packed and Union

This part only concerns advanced programming paradigms, and is close to the machine level. It is unlikely that you will ever need it, unless you try to optimize your code at a binary level.

Packed

The size of a structure is calculated by the compiler, which decides the alignment of the different fields. This is why the size of a structure containing an i64 and a c8 is 16 bytes, not 9 bytes. There is no guarantee about the size or the order of the fields in the generated program. To force the compiler to remove the optimized alignment, the special modifier packed can be used.

import std::io

struct @packed
| x : i64
| c : c8
 -> Packed;
 
struct 
| x : i64
| c : c8
 -> Unpacked;


def main () {
    println ("Size of packed : ", sizeof Packed);
    println ("Size of unpacked : ", sizeof Unpacked);
}

Results:

Size of packed : 9
Size of unpacked : 16

Union

The union special modifier , on the other hand, informs the compiler that all fields in the structure must share the same memory location. In the following example, the union modifier is used on a structure containing two fields. The largest field of the structure is the field y of type f64. The size of this field is 8 bytes, thus the structure has a size of 8 bytes as well. All the fields are aligned at the beginning of the strucures, meaning that the field x, and y shares the same address in memory.

struct @union 
| x : i32
| y : f64
 -> Dummy;


The construction of a structure with union modifier requires only one argument. This argument must be passed as a named expression with the arrow operator ->.

import std::io

struct @union
| x : i32
| y : f32
 -> Dummy;

def main ()
    throws &AssertError
{
    let x = Dummy (y-> 12.0f);

    // Comparison of pointer is only possible on pointer of the same type
    // Any pointer can be casted into a pointer of &void (the contrary is not possible)
    // is operator, checks if two pointer are equals
    assert (cast!(&void) (&(x.x)) is cast!(&void) (&(x.y)));

    // The value of x depends on the value of y
    assert (x.x == 1094713344);
    assert (x.y == 12.0f);
}

Structure specific attributes

Structures have type specific attributes, as any types, accessible with the double colon binary operator ::. The table below presents these specific attributes. These attributes are accessible using a type of struct, and not a value. A example, under the table presents usage of struct specific attributes.

Name Meaning
init The initial value of the type
typeid The name of the type stored in a value of type [c32]
typeinfo A structure of type TypeInfo, containing information about the type

All the information about TypeInfo are presented in chapter Dynamic types.

mod main;

import std::io;

struct
| x : i32
| y : i32 = 9
 -> Point; 
 
def main ()
    throws &AssertError
{
    let x = Point::init;
    
    // the structure is declared in the main module, thus its name is main::Point	
    assert (Point::typeid == "main::Point");
    
    assert (x.x == i32::init && x.y == 9);
}

Enumeration

Enumerations are user-defined types that enumerates a list of values. The keyword enum is used to define an enumeration type. The type of the fields can be inferred from the value associated to the fields. This type can be forced using the type operator :, after the keyword enum. All the fields of an enumeration shares the same type.

The complete grammar of an enumeration is presented in the following source block. As for struct declaration, templates can be used, but this functionnality will only be discussed in the Templates chapter.

enum_type := 'enum' (':' type)? (inner_value)+ '->' identifier (templates)?;
inner_value := '|' identifier '=' expression
identifier := ('_')* [A-z] ([A-z0-9_])*	


In the following source code, an example of an enumeration of type [c32] is presented. This enumeration lists the names of the days.

import std::io

enum
| MONDAY = "Mon"
| TUESDAY = "Tue"
| WEDNESDAY = "Wed"
| THURSDAY = "Thu"
| FRIDAY = "Fri"
| SATURDAY = "Sat"
| SUNDAY = "Sun"
 -> Day;

def foo (day : Day) {
    println (day);
}

def main () {
    let d = Day::MONDAY;
    foo (d);
}

Value access

The values of an enumeration are accessible using the double colon binary operator ::. In practice, access a value of the enumeration will past the content value of the field at the caller location. The value - result of the expression - is of the type of the enumeration (for example the type Day in the example below).

Value types

An example of enumeration access is presented in the following source code. In this example, implicit casting is perform from a Day to a [c32], when calling the function foo, at line 23. This implicit cast is allowed.

import std::io

enum : [c32] // the type is optional
| MONDAY = "Mon"
| TUESDAY = "Tue"
| WEDNESDAY = "Wed"
| THURSDAY = "Thu"
| FRIDAY = "Fri"
| SATURDAY = "Sat"
| SUNDAY = "Sun"
 -> Day;

def foo (day : [c32]) {
    println (day);
}

def bar (day : Day) {
    println (day);
}

def main () {
    // the internal type Day is of type [c32], so it can be implicitely casted into [c32]
    foo (Day::MONDAY);
    
    bar (Day::MONDAY);

    // However, it is impossible to transform a [c32] into a Day implicitely
    bar ("Mon")
}


However, the contrary is not allowed, because the source code tries to cast a [c32] into a Day at line 28, the compiler returns an error. The error is presented in the code block below. Such cast is forbidden, to avoid enumeration value to contain a value that is actually not defined in the list of the field of the enumeration. For example, if this was accepted, the string "NotADay" would be castable into a Day (note: the value of the string being possibly unknown at compilation time).

Error : the call operator is not defined for main::bar and {mut [c32]}
 --> main.yr:(28,9)
28  ┃     bar ("Mon")
    ╋         ^     ^
    ┃ Note : candidate bar --> main.yr:(17,5) : main::bar (day : main::Day([c32]))-> void
    ┃     ┃ Error : incompatible types main::Day and mut [c32]
    ┃     ┃  --> main.yr:(28,10)
    ┃     ┃ 28  ┃     bar ("Mon")
    ┃     ┃     ╋          ^
    ┃     ┃ Note : for parameter day --> main.yr:(17,10) of main::bar (day : main::Day([c32]))-> void
    ┃     ┗━━━━━━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Value constructions

Enumeration values can be more complex than literals. Any kind of value can be used, for example call functions, condition block, scope guards, etc. In the following example, an enumeration creating structure from function call is presented. The type of the enumeration is Ipv4Addr.

import std::io

struct
| a : i32
| b : i32
| c : i32
| d : i32
 -> Ipv4Addr;

enum
| LOCALHOST = localhost ()
| BROADCAST = broadcast ()
 -> KnownAddr;

def localhost ()-> Ipv4Addr {
    println ("Call localhost");
    Ipv4Addr (127, 0, 0, 1)
}

def broadcast ()-> Ipv4Addr {
    println ("Call broadcast");	
    Ipv4Addr (255, 255, 255, 255)
}

def main ()
    throws &AssertError
{
    let addr = KnownAddr::LOCALHOST; // will call localhost here

    assert (KnownAddr::LOCALHOST.a == 127); // a second time here
    assert (KnownAddr::BROADCAST.d == 255); // call broadcast here
    assert (addr.a == 127);
}


The enumeration value of a field is constructed at each access, this means for example that when enumeration values are constructed using function call, the function is called each time the enumeration field is accessed. Thus the result of the execution of the compiled source code above is the following:

Call localhost
Call localhost
Call broadcast


Value context

If the value of the enumeration seems to be passed at the caller location, they don't share the context of the caller. In other words, the fields of an enumeration have access to the symbol accessible from the enumeration context, and not from the caller context. An example, of enumeration trying to access symbols is presented in the source code bellow.

import std::io

static __GLOB__ = true;

enum 
| FOO = (if (x) { 42 } else { 11 })
| BAR = (if (__GLOB__) { 42 } else { 11 })
 -> ErrorEnum;
 
def main () {
    let x = false;
    
    println (ErrorEnum::FOO); 
}


From the above example, the compiler returns an error. In this error, the compiler informs that the variable x is not defined from the context of the enumeration. Even if the variable is declared inside the main function, it is not accessible from the enumeration context. The global variable __GLOB__ is accessible from the enumeration context, and thus accessing it is not an issue.

Note : 
 --> main.yr:(6,3)
 6  ┃ | FOO = (if (x) { 42 } else { 11 })
    ╋   ^^^
    ┃ Error : undefined symbol x
    ┃  --> main.yr:(6,14)
    ┃  6  ┃ | FOO = (if (x) { 42 } else { 11 })
    ┃     ╋              ^
    ┗━━━━━┻━ 

Note : 
 --> main.yr:(13,11)
13  ┃ 	println (ErrorEnum::FOO); 
    ╋ 	         ^^^^^^^^^
    ┃ Error : the type main::ErrorEnum is not complete due to previous errors
    ┃  --> main.yr:(8,5)
    ┃  8  ┃  -> ErrorEnum;
    ┃     ╋     ^^^^^^^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(13,11)
    ┃     ┃ 13  ┃ 	println (ErrorEnum::FOO); 
    ┃     ┃     ╋ 	         ^^^^^^^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Enumeration specific attributes

As for any type, enumeration have specific type attributes. The table below lists the enumeration type specific attributes.

Name Meaning
members A slice containing all the values of the enumeration
member_names A slice of [c32], containing all the names of the fields of the enumeration (same order than members)
typeid The type identifier of the enumeration type in a [c32] typed value
typeinfo The typeinfo of the inner type of the enumeration (cf. Dynamic types)
inner The inner type of the enumeration


One may note that the operator to access specific attributes and field is the same (double colon binary operator ::), and therefore that if an enumeration have a field named as a specific attributes there is a conflict. To avoid conflict, the priority is given to the fields of the enumeration, and specific attributes can be accessed using their identifier surrounded by _ tokens. For example, accessing the members specific attributes can be made using the identifier __members__. An example of the principle is presented in the following source code. The specific attribute surrounding is applicable to all types, but can be really usefull here.

mod main;

enum
| typeid       = 1
| typeinfo     = 2
| members      = 3
| member_names = 4
| inner        = 5
 -> AnnoyingEnum;

def main ()
    throws &AssertError
{
    assert (AnnoyingEnum::typeid == 1);
    assert (AnnoyingEnum::__typeid == AnnoyingEnum::__typeid__);
    assert (AnnoyingEnum::__typeid == "main::AnnoyingEnum");	
}

AKA

An aka create a symbol that is an alias for another expression. Everything can be used to create an alias, not necessarily a value, or a type. The keyword aka was choosed to avoid confusion with alias, that has a completely different meaning in Ymir.

The grammar of akas is presented in the following code block.

aka_decl := 'aka' Identifier (templates)? = expression (';')?

Aka as a value

Aka can be used as a single value enumeration. As for enumeration values, akas are constructed at each access, and are for that reason closer to enumeration, than to global variable. In addition, akas are only defined during compilation time, and are not defined inside the executable, library, etc. generated by the compiler, contrary to global variables.

An example of an aka is presented in the source code below. In that example, the aka refers to the call of the foo function, that is called each time the aka is used.

import std::io

aka CallFoo = foo ()

def foo () -> i32 {
    println ("Foo");
    42
}

def main () {
    println (CallFoo);
    println (CallFoo);
}


Results:

Foo
42
Foo
42


Because akas are not global variable, they don't have an address, and are always immutable. They don't really have a type, and simply stick their content, at the location of the caller. However, like enumeration values, their context is the one of their declaration, not the context of the caller. For that reason by compiling the following example, the compiler returns an error.

import std::io

aka FOO = x // x is not defined in this context

def main () {
    let x = 12; // this x is local, and not accessible from FOO
    println (FOO); // does not work
}


Error:

Error : undefined type x
 --> main.yr:(3,11)
 3  ┃ aka FOO = x
    ╋           ^

Aka as a type

Akas do not always referes to values, but can also refer to type. The symbol access rules are the same as value akas.

import std::io

aka MyTuple = (i32, i32)

def foo (a : MyTuple) {
    println ("x: ", a.0, ", y: ", a.1);
}

def main () {
    let x = (1, 2);
    foo (x);
}


Results:

x: 1, y: 2


Akas type are not real type, meaning that the definition of foo in the previous example, is strictly equivalent to

def foo (a : (i32, i32)) {
    println ("x: ", a.0, ", y: ", a.1);
}

Aka as symbols

Akas are not confined to type, and values. They can create symbols to refer to other symbols. For example modules, functions, structures, etc.

import std::io

aka IO = std::io

def println (a : i32) {
    IO::println ("My println : ", a);
}

def main () {
    IO::println (12);
    println (12);
}


Results:

12
My println : 12


Contribution : enable aka on import, with the syntax : import path aka name. This is already possible, as we can see in the previous example, but needs two lines.

Objects

Ymir is a object oriented language, with polymorph types. In this chapter, we assume that you are familiar with object oriented programming paradigm (if not, you will benefit from reading information on that paradigm first cf. object oriented programming.).

In Ymir, object instances are dynamically allocated on the heap. Indeed, because object types are polymorph, their size cannot be known at compile time, and consequently cannot be placed in the stack. To illustrate this point, the following figure presents the UML definition of a class A, and an heir class B. The class A contains a field x, of type i32, thus of size 4 bytes. The class B, contains another field y of size 4, leading to a class B of size 8.


drawing

Because we want to following source code to be accepted (basic principle of polymorphism), the size of A, cannot be statically set to 4 bytes, (nor to 8, the class B can come from a totally different part of the source code - from a library for example -, and unknown when using the type A, without talking of the huge loss of memory if only A values are used).

def foo (a : &(A)) // ...	

def main () {
    let b : &B = // ...
    foo (b); // calling foo with a B, heir of A
}


For that reason, object instances are aliasable types, and only contains a pointer to the values of the class. You may have notice the syntax &(A), in the above example. If this syntax is similar to pointer, this is because objects are basically pointers. However, unlike basic pointers, these cannot be used in pointer arithmetics, cannot be null, and does not need to be dereferenced to access the value. In other words, these are reference, more than pointers. Yes, unlike other object oriented language such as java or D, object instances cannot be null. This is a really important part of the object system in Ymir guaranteeing that every objects are pointing to a valid value, and that value is correctly initialized (constructor was called).

We will see in the coming chapters, that removing the possibility of null objects does not remove any capacity on the language, while adding strong safety, the number one error of java programs being NullPointerException (cf. Which Java exceptions are the most frequent?, The Top 10 Exception Types in Production Java Applications – Based on 1B Events).

Class

An object is an instance of a class. A class is declared using the keyword class, followed by the identifier of the class. The complete grammar of a class definition is presented in the following code block.

class_decl := simple_class_decl | template_class_decl

simple_class_decl := 'class' (modifiers)? Identifier ('over' type)? '{' class_content '}'
template_class_decl := 'class' ('if' expression)? (modifiers)? Identifier templates ('over' type)? '{' class_content '}'
class_content :=   field
                 | method
                 | constructor
                 | impl
                 | destructor
modifiers := '@' ('{' modifier (',' modifier)* '}') | (modifier)
modifier := 'final' | 'abstract'


As many symbols, the class can be a template symbols. Templates are not presented in this chapter, and are left for a future chapter (cf. Templates).

Fields

A class can contain fields. A field is declared as a variable, using the keyword let. A field can have a default value, in that case the type is optional, however if the value is not set, the field must have a type. Multiple fields can be declared within the same let, with coma separating them.

class Point4D {
    let _x : i32;
    let _y = 0;
    let _z : i32, _w = 0;
}

Field privacy

All fields are protected by default, i.e. only the class defining them and its heirs have access to them. The keyword pub can be used to make the fields public, and accessible from outside the class definition. The keyword prv, on the other hand, can be use to make the field totally hidden from outside the class. Unlike prot, prv fields are not accessible by the heirs of a class.

A good practice is to enclose the privacy of the fields in their name definition. For example, a public field is named x, without any _ token. A protected fields always starts with a single underscore, _y, and private fields are surrounded by two underscores before and after the identifier. This is just a convention, the name has no impact on the privacy.

class A {
    pub  let x = 12;
    prot let _y : i32;
    prv  let __z__ : i32;
}

Constructor

To be instancible, a class must declare at least one constructor. The constructor is always named self, and takes parameters to build the object. The object is accessible within the constructor body through the variable self.

Field construction

The constructor must set the value to all the fields of the class, with the keyword with. Fields with default values are not necessarily set by this with statement, but can be redefined. The with statement has the effect of the initial value of the fields, meaning that the value of immutable fields is set by them.

class Point {
    let _x : i32;
    let _y = 0;
    
    /**
     * _y is already initialized
     * it is not mandatory to add it in the with initialization
     */
    pub self () with _x = 12 {}
    
    /**
     * But it can be redefined
     */
    pub self (x : i32, y : i32) with _x = x, _y = y {}
}


Field value is set only once. For example, if a class has a field _x with a default value, calling a function foo. And the constructor use the with statement to set the value of the field from the return value of the bar function, the function foo is never called.

def foo () -> i32 {
    println ("Foo");
    42
}

def bar () -> i32 {
    println ("bar")
    33
}

class A {
    let _x = foo ();
    
    pub self () with _x = bar () {} // foo is not called, bar is call instead
}


The point behind with field construction, is to ensure that all fields have a value, when entering the constructor body. This way, when instruction are made inside the constructor body, such as printing the value of the fields, their value is already set, and the object instance is already valid.

class A {
    let _x : i32;
    
    pub self () with _x = 42 {
        println (self._x); // access the field _x, of the current object instance
    } 
}

Create an object instance

Class are used to create object instance, by calling a constructor. This call is made using the double colon binary operator ::, with the left operand being the name of the class to instantiate, and the right operand the keyword new. After the keyword new a list of argument is presented, this list is the list of argument to pass to the constructor. The constructor with the best affinity is choosed and called on a allocated instance of the class. Constructor affinity is computed as function affinity (based on type mutability, and type compatibility – cf. Aliases and References).

import std::io

class A {

    pub self (x : i32) {
        println ("Int : ", x);
    }
    
    pub self (x : f32) {
        println ("Float : ", x);
    }

}

def main () {
    let _ = A::new (12);
    let _ = A::new (12.f);
}


Results

Int : 12
Float : 12.000000

Named constructor

A name can be added to the constructor. This name is an Identifier, and follows the keyword self. A named constructor can be called by its name when constructing a class. This way two constructor can share the same prototype, and be called from their name. A named constructor is not ignored when constructing a class with the new keyword, its named is just not considered.

In the following example, a class A contains two constructors, foo and bar, these constructors have one parameter of type i32.

import std::io

class A {
    let _x : i32;
    
    pub self foo (x : i32) with _x = x {
        println ("Foo ", self._x);
    }
    
    pub self bar (x : i32) with _x = x {
        println ("Bar ", self._x);
    }
}

def main () {
    let _ = A::foo (12);
    let _ = A::bar (12); 
    
    let _ = A::new (12);
}


The last object construction at line 19 is not possible, the call working with both foo and bar. The other constructions work fine, that is why the compiler returns only the following error.

Error : {self foo (x : i32)-> mut &(mut main::A), self bar (x : i32)-> mut &(mut main::A)} x 2 called with {i32} work with both
 --> main.yr:(19,17)
19  ┃ 	let _ = A::new (12);
    ╋ 	               ^
    ┃ Note : candidate self --> main.yr:(6,6) : self foo (x : i32)-> mut &(mut main::A)
    ┃ Note : candidate self --> main.yr:(10,6) : self bar (x : i32)-> mut &(mut main::A)
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.

Construction redirection

To avoid redondent code, a constructor can call another constructor in the with statement. This call redirection is performed by using the self keyword. In that case, because the fields are already constructed by the called constructor, they must not be reconstructed.

import std::io;

class A {
    let _x : i32;

    pub self () with self (12) {
        println ("Scd ", self._x);
    }

    pub self (x : i32) with _x = x {
        println ("Fst ", self._x);
    }
    
}


def main () {
    let _ = A::new (); // construct an instance of the class A
                       // from the constructor at line 6
}


Results:

Fst 12
Scd 12


Contribution: Redirection to named constructor does not work for the moment. This is not complicated, but has to be done.

Constructor privacy

As fields, the privacy of the constructor is protected by default. The keyword prv and pub can be used to change it.

Destructors

Object instances are destroyed by the garbage collector. Meaning that there is no way to determine when or even if an object instance will be destroyed. But lets say that such operation effectively happen (which is quite probable, let's no lie either), then a last function can be called just before the object instance is destroyed and irrecoverable. The destructor is called __dtor, and takes the parameter mut self. There is only one destructor per class, this destructor is optional and always public.

import std::io;

static mut I = 0;

class A {
    let _x : i32;
    pub self () with _x = I {
        I += 1;
    }
    
    __dtor (mut self) {
        println ("Destroying me ! ", self._x); 
    }

}

def foo () {
    let _ = A::new (); 
} // the object instance is irrecoverable at the end of the function

def main () {
    loop {
        foo ();
    }
}


Results:

Destroying me ! 765
Destroying me ! 1022
Destroying me ! 764
Destroying me ! 1023
Destroying me ! 763
Destroying me ! 1024
Destroying me ! 762
Destroying me ! 1025
Destroying me ! 761
Destroying me ! 1026
...


One can note from the result, that the order of destruction is totally unpredictible. Rely on class destructor is not the best practice. We will see in a future chapter disposable objects, that are destroyed in a more certain way. Destructors are a last resort to free unmanaged memory, if this was forgotten (for example, file handles, network socket, etc. if not manually disposed).

Mutability

Objects are aliasable types. The data being allocated on the heap, and not copied - for efficiency reasons - on affectations (cf. Aliases and References).

Object mutability

The type of an object instance is a reference to a class type. This reference can mutable, and the data pointed data as well. In the following example, a variable a containing an object instance of the class A is created. This variable contains a mutable reference, but the data borrowed by the reference are not mutable.

class A {
    
    pub let mut x : i32;
    
    pub self (x : i32) with x = x {}
    
}


def main () {
    let mut a : &(A)= A::new (12);
    
    a = A::new (42); // ok the reference if mutable
    
    a.x = 33; // the borrowed data are not mutable
}


Because the data borrowed by a are not mutable, the compiler returns an error.

Error : left operand of type i32 is immutable
 --> main.yr:(15,3)
15  ┃ 	a.x = 33; // the borrowed data are not mutable
    ╋ 	 ^


ymir1: fatal error: 
compilation terminated.


This error can be avoided, by making the borrowed data mutable as well. Either by using the dmut modifier, or by writing mut &(mut A).

def main () {
    let dmut a = A::new (12);
    a.x = 42; // no problem a is mutable, and its borrowed data as well.
    println (a.x); 
}

Because classes are aliasable types, the keyword alias has to be used when trying to make a mutable affectation. Information about aliasable types is presented in chapter Aliases, and is not rediscussed here.

def main () 
    throws &AssertError
{
    let dmut a = A::new (12);
    let dmut b = alias a;
    b.x = 42;
    
    assert (a.x == 42);
}

Field mutability

Field mutability is set once and for all, in the definition of the class. The information about field mutability presented in the chapter about structure is applicable to classes.

However, unlike structures, classes are not copiable by default, but have to implement a specific traits. Trait and implementation is presented in a future chapter, and copy is discussed in there chapter. (cf. Traits).

class A {
    
    pub let mut x : i32;
    pub let y : i32;
    
    pub self (x : i32, y : i32) with x = x, y = y {}
    
}


def main () {
    let dmut a = A::new (0, 1);
    a.x = 43;
    a.y = 89;
}


Errors:

Error : left operand of type i32 is immutable
 --> main.yr:(14,3)
14  ┃ 	a.y = 89;
    ╋ 	 ^


ymir1: fatal error: 
compilation terminated.

Methods

Methods are functions associated to object instances. Methods are described inside a class definition. Information about function presented in chapter Functions are applicable to methods. The grammar of a method is presented in the following code block.

method := simple_method | template_method

simple_method := 'def' Identifier method_params ('->' type)? expression
template_method := 'def' ('if' expression)? Identifier templates  ('->' type)? expression

method_params := '(' ('mut')? 'self' (',' param_decl)* ')'


Methods are accessible using an object instance, of the dot binary operator .. Once accessed, a method can be called using a list of arguments separated by comas inside parentheses. The first parameter of a method is the object instance, and is the left operand of the dot operator, so it must not be repeated inside the parentheses.

import std::io

class A {
    let _a : i32;

    pub self (a : i32) with _a = a {}
    
    
    pub def foo (self, x : i32) -> i32 {
        println ("Foo ", self._a);
        x + self._a 
    }
}

def main () {
    let a = A::new (29);
    println (a.foo (13));
}


Results:

Foo 29
42


The access to the fields, and to the methods of the object instance inside the body of a method is made using the first parameter of the method, the variable self. Unlike some object oriented language, such as Java, C++, Scala, D, etc. self is never implicit, e.g. accessing to the field _a in the above example, cannot be made by just writing _a, but must be accessed by writing self._a. This is a conceptual choice, whose purpose is to improve code readability and sharing, by avoiding useless and preventable search for the source of the variables.

Privacy

Methods are protected by default. Meaning that only the class that have defined them, and its heir class have acces to them. The keyword pub and prv can be used to change the privacy of a method. A public method is accessible everywhere, using an object instance, and private methods are only accessible by the class that have defined them. Unlike protected methods, private methods are not accessible by heir classes. Privacy of methods is the same as privacy of fields.

import std::io

class A {

    pub self () {}
    
    pub def foo (self) {
        println ("Foo");
        self.bar ();
    }
    
    prv def bar (self) {
        println ("bar");
    }
}


def main () {
    let a = A::new ();
    a.foo ();
    a.bar ();
}


Because the method bar is private in the context of the main function, the compiler returns the following error. One can note, that the compiler tried to rewrite the expression into a uniform call syntax (i.e. bar (a)), but failed, because the function bar does not exists.

Error : undefined field bar for element &(main::A)
 --> main.yr:(21,3)
21  ┃ 	a.bar ();
    ╋ 	 ^^^^
    ┃ Note : bar --> main.yr:(12,10) : (const self) => main::A::bar ()-> void is private within this context
    ┃ Note : when using uniform function call syntax
    ┃ Error : undefined symbol bar
    ┃  --> main.yr:(21,4)
    ┃ 21  ┃ 	a.bar ();
    ┃     ╋ 	  ^^^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Method mutability

The mutability of the object instance must be defined in the prototype of the method. By default the object instance refered to by self is immutable, meaning that the instance cannot be modified.

class A {
    
    let mut _x : i32;
    
    pub self (x : i32) with _x = x {}
    
    pub def setX (self, x : i32) {
        self._x = x;
    }	
    
}


Error:

Error : when validating main::A
 --> main.yr:(1,7)
 1  ┃ class A {
    ╋       ^
    ┃ Error : left operand of type i32 is immutable
    ┃  --> main.yr:(8,7)
    ┃  8  ┃ 		self._x = x;
    ┃     ╋ 		    ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


By using the keyword mut, the method can be applicable to mutable instance. In that case the instance refered to by self is mutable, and its mutable fields can be modified. Such methods can be accessed only using mutable object instance - not only mutable reference, the borrowed data located on the heap must be mutable. To call such method, aliases is necessary and cannot be implicit.

class A {
    let mut _x : i32;
    
    pub self (x : i32) with _x = x {}
    
    pub def setX (mut self, x : i32) {
        self._x = x;
    }	
    
}

def main () {
    let a = A::new (12);
    a.setX (42); 
    
    let dmut b = A::new (12);
    b.setX (42);
    
    (alias b).setX (42);	
}


The first call at line 14 is not possible, a having only read access to the object instance it contains. The second error, the call at line 17, is due to the fact that even b have write permission to the object instance it contains, it cannot be passed to the method implicitely, and have to be aliased, in order to certify explicitely that the user is aware that the value of the object contained in b will be modified by calling the method. The errors returned by the compiler are the following.

Error : the call operator is not defined for (a).setX and {i32}
 --> main.yr:(14,9)
14  ┃ 	a.setX (42); 
    ╋ 	       ^  ^
    ┃ Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
    ┃  --> main.yr:(14,9)
    ┃ 14  ┃ 	a.setX (42); 
    ┃     ╋ 	       ^
    ┃     ┃ Note : implicit alias of type &(main::A) is not allowed, it will implicitly discard constant qualifier
    ┃     ┃  --> main.yr:(14,2)
    ┃     ┃ 14  ┃ 	a.setX (42); 
    ┃     ┃     ╋ 	^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 

Error : the call operator is not defined for (b).setX and {i32}
 --> main.yr:(17,9)
17  ┃ 	b.setX (42);
    ╋ 	       ^  ^
    ┃ Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
    ┃  --> main.yr:(17,9)
    ┃ 17  ┃ 	b.setX (42);
    ┃     ╋ 	       ^
    ┃     ┃ Note : implicit alias of type mut &(mut main::A) is not allowed, it will implicitly discard constant qualifier
    ┃     ┃  --> main.yr:(17,2)
    ┃     ┃ 17  ┃ 	b.setX (42);
    ┃     ┃     ╋ 	^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


We have seen in the chapter about function, and more specifically about uniform call syntax that the operator :. can be used to pass a aliased value as the first parameter of a function. This operator is also applicable for method calls, and is to be prefered to the syntax (alias obj).method which is a bit verbose.

def main () {
    let dmut a = A::new (12);
    
    a:.setX (42); // same as (alias a).setX (42)
}


Method mutability is also applicable inside the body of a method. In the example below, the method foo is mutable, and call another mutable method bar, it also calls a immutable method baz. Implicit aliasing is mandatory when calling the method bar, but not when calling the method baz.

class A {
    let _x : i32;
    pub self (x : i32) with _x = x {}

    pub def foo (mut self, x : i32) {
        self:.bar (x); // :. is mandatory
        self.baz (x);
    }
    
    pub def bar (mut self, x : i32) {
        self._x = x;
    }

    pub def baz (self) {
        println ("X : ", self._x);
    }
}


Contribution: Calling a immutable method with explicit alias is possible. Maybe that is not a good idea, and will lead to the use of the operator :. all the time, and misleading read of the code.

Method mutability override

It is possible to define two methods with the same prototype, with the only exception that one of them is mutable and not the other. In that case the method with the best affinity is choosed when called. That is to say, the mutable method is called on explicitly aliased object instances, and the immutable method the rest of the time.

import std::io

class A {

    pub self () {}
    
    pub def foo (mut self) {
        println ("Mutable");
    }
    
    pub def foo (self) {
        println ("Const");
    }   	
}

def main () {
    let dmut a = A::new ();
    a.foo ();
    a:.foo ();
}


Results:

Const
Mutable

Inheritance

One of the most important point of the object oriented programming paradigm is the possibility for a class to be derived from a base class. This capability enables type polymorphism. In Ymir the keyword over is used for class derivation, and overriding. A class can have only one ancestor. We will see in the chapter about traits, that multiple inheritance can be in some way achieved in another way.

class Shape {
}

/**
 * A circle is a shape
 */
class Circle over Shape {
    let _center : i32 = 0;
    let _radius : i32 = 1;
}

Fields

The fields of an ancestor cannot be redeclared by an heir class. Even if they are hidden to the heir class (private fields). This choice was made to avoid miscomprehension, as two different variables would be named the same way.

import std::io;
        
class Shape {
    let _x : i32 = 0;
    
    pub self () {}
}

class Circle over Shape {
    let _x : i32;
    
    pub self () {
        println (self._x);
    }	
}


Error:

Error : when validating main::Circle
 --> main.yr:(9,7)
 9  ┃ class Circle over Shape {
    ╋       ^^^^^^
    ┃ Error : declaration of _x shadows another declaration
    ┃  --> main.yr:(4,13)
    ┃  4  ┃     prv let _x : i32 = 0;
    ┃     ╋             ^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(10,9)
    ┃     ┃ 10  ┃     let _x : i32;
    ┃     ┃     ╋         ^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Parent class construction

All object instances must be constructed. This means that when a class override a base class, the constructor of the base class must be called inside the constructor of the heir class. This call can be made implicitly if the constructor of the parent class takes no arguments. However, if no constructor in the parent class takes no arguments, then the call must be made explicitly. This explicit call is made inside the with statement.

class Shape {	
    let _x = 0;
    pub self () {}
    
    pub self (x : i32) with _x = x {}
}

class Circle over Shape {
    let _radius : f32;
    
    // Same as : with super ()
    pub self () with _radius = 1.0f {} 
    
    // call the second parent constructor, at line 5
    pub self (x : i32) with super (x), _radius = 1.0f {} 
}


The call of the parent constructor is the first thing performed inside a constructor. Meaning, that the construction of the fields of the heir class are made afterward. To illustrate this point, the following example presents a base class Foo with a constructor printing foo, and a heir class Bar, calling the function bar* (printing the value bar) to initialize its field _x.

import std::io;

def bar () -> i32 {
    println( "Bar");
    42
}

class Foo {    
    pub self () {
        println ("Foo");
    }
}

class Bar over Foo {
    let _x : i32;

    pub self () with _x = bar () {}
}

def main () {
    let _ = Bar::new ();
}


Results:

Foo
Bar


However, because constructor redirection does not call parent constructor, and let that work to the called constructor, to which they are redirected, the following program has a different behavior. The arguments of the redirection are computed first, then the call to the parent constructor, and finally the construction of the fields of the heir class.

import std::io;

def bar () -> i32 {
    println( "Bar");
    29
}

def baz () -> i32 {
    println( "Baz");
    13
}

class Foo {
    pub self () {
        println ("Foo");
    }
}

class Bar over Foo {
    let _x : i32;

    /**
     * Call parent constructor at line 14, and then bar
     */
    pub self (x : i32) with _x = bar () + x {}

    /**
     * Call baz first, then self at line 25
     */
    pub self () with self (baz ()) {} 
}

def main () {
    let _ = Bar::new ();
}


Results:

Baz
Foo
Bar


The construction order is perfectly predictible, but should not have an impact on the program behavior. So it is probably not a good idea to rely on it.

Parent class destruction

In contrast to construction, the parent destructor is the last operation of the destruction of a heir class. The parent destructor is always called, there is no way to avoid it (aside exiting the program).

import std::io;

class Foo {    
    pub self () {}
    __dtor (mut self) {
        println ("Destroying Foo");
        println ("====");
    }
}

class Bar over Foo {
    pub self () {}

    __dtor (mut self) {
        println ("====");
        println ("Destroying Bar");
        return {} // desperately trying to avoid parent destruction
    }
}

def foo () {
    let _ = Bar::new ();
}

def main () {
    loop {
        foo ();
    }
}


Results:

====
Destroying Bar
Destroying Foo
====
====
Destroying Bar
Destroying Foo
====
====
Destroying Bar
Destroying Foo
====
...

Method overriding

The keyword over is used to override a method. Methods cannot be implicitly overriden by omitting the over keyword and using the def keyword. The signature of the method must be strictly identical to the one of the ancestor method, including privacy, and argument mutability. Of course, private methods cannot be overriden, because hidden to heir classes, but protected and public methods can be overriden. In the following example, a class Shape define the method area. This method is public, and then can be overriden by heir classes. The class Circle and Rectangle overrides the methods.

import std::io
    
class Shape {
    pub self () {}
    
    pub def area (self) -> f64 
    0.0
}

class Circle over Shape {
    let _radius : f64;
    pub self (radius : f64) with _radius = radius {}
    
    pub over area (self) -> f64 {
        import std::math;
        math::PI * (self._radius * self._radius)
    }
}

class Square over Shape {
    let _side : f64;
    pub self (side : f64) with _side = side {}
    
    pub over area (self) -> f64 {
        self._side * self._side
    }
    
}

def main () {
    let mut s : &Shape = Circle::new (12.0);
    println (s.area ());
    
    s = Square::new (3.0);
    println (s.area ());	
}


Results:

452.389342
9.000000

Override mutable method

The mutability of the method must be respected in the heir class. This means that mutable method must be mutable in the heir, and immutable methods must be immutable in the heir.

class Foo {
    pub self () {}
    
    pub def foo (mut self)-> void {}
    
    pub def bar (self)-> void {}
}

class Bar over Foo {
    pub self () {}
    
    pub over foo (self)-> void {}
    
    pub over bar (mut self)-> void {}
}


Errors:

Error : when validating main::Bar
 --> main.yr:(9,7)
 9  ┃ class Bar over Foo {
    ╋       ^^^
    ┃ Error : the method (const self) => main::Bar::foo ()-> void marked as override does not override anything
    ┃  --> main.yr:(12,11)
    ┃ 12  ┃ 	pub over foo (self)-> void {}
    ┃     ╋ 	         ^^^
    ┃     ┃ Note : candidate foo --> main.yr:(4,10) : (mut self) => main::Foo::foo ()-> void
    ┃     ┗━━━━━━ 
    ┃ Error : the method (mut self) => main::Bar::bar ()-> void marked as override does not override anything
    ┃  --> main.yr:(14,11)
    ┃ 14  ┃ 	pub over bar (mut self)-> void {}
    ┃     ╋ 	         ^^^
    ┃     ┃ Note : candidate bar --> main.yr:(6,10) : (const self) => main::Foo::bar ()-> void
    ┃     ┗━━━━━━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Final methods

A base class can flag its method to avoid overriding. This flag is placed as a custom modifier before the name of the method.

class Foo {
    pub self () {}
    
    pub def @final foo (self) {}
}

class Bar over Foo {
    pub self () {}

    pub over foo (self) {}
}


Errors:

Error : when validating main::Bar
 --> main.yr:(7,7)
 7  ┃ class Bar over Foo {
    ╋       ^^^
    ┃ Error : cannot override final method (const self) => main::Foo::foo ()-> void
    ┃  --> main.yr:(10,14)
    ┃ 10  ┃     pub over foo (self) {}
    ┃     ╋              ^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(4,20)
    ┃     ┃  4  ┃     pub def @final foo (self) {}
    ┃     ┃     ╋                    ^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


This flag can also be used on an overriden method inside a heir class to avoid further overriding.

class Foo {
    pub self () {}
    
    pub def foo (self) {}
}

class Bar over Foo {
    pub self () {}

    pub over @final foo (self) {}
}

class Baz over Bar {
    pub self () {}

    pub over foo (self) {}
}


Errors:

Error : when validating main::Baz
 --> main.yr:(13,7)
13  ┃ class Baz over Bar {
    ╋       ^^^
    ┃ Error : cannot override final method (const self) => main::Bar::foo ()-> void
    ┃  --> main.yr:(16,14)
    ┃ 16  ┃     pub over foo (self) {}
    ┃     ╋              ^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(10,21)
    ┃     ┃ 10  ┃     pub over @final foo (self) {}
    ┃     ┃     ╋                     ^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Abstract class

A class can be abstract, this means that the class cannot be instantiated even if it has a constructor. An abstract class can declare methods without body, these methods must be overriden by heir classes. An abstract must have a constructor to be heritable, this constructor being called by the heir classes. Not that an abstract class can have no public constructors, but that a class that have no public constructors is not necessarily abstract.

class @abstract Shape {
    prot self () {} // need a constructor to be inheritable
    
    pub def area (self)-> f64; // Method does not need a body
}

class Circle over Shape {
    let _radius : f64;
    pub self (radius : f64) with _radius = radius {}
    
    pub over area (self) -> f64 {
        import std::math;
        math::PI * (self._radius * self._radius)
    }
}

def main () {
    let s : Shape = Circle::new (12);
    println (s.area ());
}

Method with no body

A method of an abstract class can have a body, and thus behave as any method of any class. It can also have no body, but in that case heir class must override this method. Otherwise the class is incomplete. Abstract class can be heir class, in that case they don't need to override the methods without body.

class @abstract Foo {
    pub self () {}
    
    pub def foo (self);
}

class @abstract Bar over Foo {
    pub self () {}
}

class Baz over Bar {
    pub self () {}
}


Error:

Error : when validating main::Baz
 --> main.yr:(11,7)
11  ┃ class Baz over Bar {
    ╋       ^^^
    ┃ Error : the class main::Baz is not abstract, but does not override the empty parent method (const self) => main::Foo::foo ()-> void
    ┃  --> main.yr:(11,7)
    ┃ 11  ┃ class Baz over Bar {
    ┃     ╋       ^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(4,13)
    ┃     ┃  4  ┃     pub def foo (self);
    ┃     ┃     ╋             ^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Final class

Final classes declared with the custom attributes @final defines classes that cannot have heirs. A final class, can be an heir class, or a base class. If the class is a base class, strong optimization can be made by the compiler, (no vtable required, and call of the methods are direct and way faster). For that reason, it is a good practice to flag classes for which we are certain they cannot be inheritable. This optimization is also done on final methods (if they are not overriden, i.e. final when define for the first time), thus this is a good practice to flag methods for which we are certain they won't be overriden. This optimization cannot be done if the class is not a base class.

class @final Foo {
    pub self () {}
    
    pub def foo (self) {}
}

class Bar over Foo {
    pub self () {}
}


Error:

Error : the base class main::Foo is marked as final
 --> main.yr:(7,16)
 7  ┃ class Bar over Foo {
    ╋                ^^^


ymir1: fatal error: 
compilation terminated.


Contribution: It is possible to have an abstract and final class. I didn't find any use case for that, maybe that is completely useless, and must be prohibited.

Casting base class objects to heir class

In many languages (such as C++, D, Java, or Scala) polymorphism gives the possibility to cast an object of a base class into an object of an heir class. This is not possible in Ymir because this behavior is not safe. We will see in the chapter Pattern matching how to achieve a cast of an object into a heir class, in a safe way.

However, the std provides a safe shortcut that can be used to achieve the cast. This shortcut is by using the template function to of the module std::conv. This function throws a CastFailure exception when the cast failed, (safe in Ymir means that the error can be managed, and has to be managed in fact, as we will see in the chapter on Error handling). In the following example, two objects are stored in the variable x and y, whose type are &Foo. The first cast at line 18 works, because the variable x indeed contains an object of type &Bar, however the cast at line 19 does not work, the variable y stores an object of type &Foo.

import std::conv;

class Foo {
    pub self () {}
}

class Bar over Foo {
    pub self () {}
}


def main ()
    throws &CastFailure // the possible errors are rethrown, so the program ends if there is an error 
{
    let x : &Foo = Bar::new ();
    let y : &Foo = Foo::new ();
    
    let _ : &Bar = x.to!(&Bar) (); // possibly throw a &CastFailure
    let _ : &Bar = y.to!(&Bar) (); // here as well, (and actually throw it)
}


The following result happens because an error is thrown by the main function, and then unmanaged by the program. The stacktrace is printed because the program was compiled in debug mode. We can see in this trace (at line 11) that the error was effectively thrown by the conversion at line 19.

Unhandled exception
Exception in file "/home/emile/gcc/gcc-install/bin/../lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/ymir/std/conv.yr", at line 820, in function "std::conv::to(&(main::Bar),&(main::Foo))::to", of type std::conv::CastFailure.
╭  Stack trace :
╞═ bt ╕ #1
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3 in function std::conv::toNP94main3BarNP94main3Foo::to (...)
│     ╘═> /home/emile/gcc/gcc-install/bin/../lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/ymir/std/conv.yr:820
╞═ bt ╕ #4 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:19
╞═ bt ╕ #5
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #6 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #7
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #8 in function _start
╰
Aborted (core dumped)

Trait

Traits are used to define as their name suggest traits that can be implemented by a class. To define them, the keyword trait is used. A trait is not a type, and can only be implemented by class, for that reason, a variable or a value cannot be of a trait type.

An example of a trait, is presented in the following source block. The idea of this trait, is to ensure that every class, implementing it, have a public method named print that can be called without parameters.

trait Printable {
    pub def print (self);	
}


When a class implements a trait, all the method declared in the trait are added in the definition of the class. If the class is not abstract all the method of the traits must have a body. For example, if a non abstract class implement the trait Printable, thus it must override the method print to add a body to it.

import std::io

class Point {
    // ... Constructors and attributes
    
    impl Printable {
        // Try to remove the following definition
        pub over print (self) {
            print ("Point {", self._x, ", ", self._y, "}");
        }
    }	
}


A trait can provide a default behavior for the method it defines. The body of the method is validated for each implementation. In the following example, the method print defined in the trait Printable, prints the typeid of the class that implement the traits. One can note, that the behavior of this function is different for each class that implements it, that is why it is only validated when a class implement the trait.

mod main; 

trait Printable {
    pub def print (self) {
        import std::io;
        print (typeof (self)::typeid);
        // Here we don't know the type yet
    }
}

class Point {
    // ... Constructors and attributes

    impl Printable;  // 'print' method prints ("main::Point")
}

class Line {
    // ... Constructors and attributes

    impl Printable;  // 'print' method prints ("main::Line")
}


Warning: If a trait is never implemented by any class, and have methods with default behavior, then it is never validated. Thus errors can be present in this trait, but still pass the compilation. One can see the trait as a kind of template, this problem being present in template symbol as well (cf. Templates).

Inherit a Class implementing a Trait

The methods of a trait can be overriden by heir classes. In order to do this, heir classes must reimplement the trait, and override the methods.

Simple reimplementation

In the following example, the class Shape implements the trait Printable, this trait has a method print with a default behavior. The class Circle does not reimplement the trait, thus when calling the method print of a Circle value, the value main::Shape is printed on the stdout. On the other hand, the class Rectange reimplement the traits, thus the value main::Rectangle is printed.

mod main;

trait Printable {
    pub def print (self) {
        import std::io;
        println (typeof (self)::typeid);
    }
}

class @abstract Shape {
    self () {}
    
    impl Printable;
}

class Circle over Shape {
    pub self () {}
}

class Rectangle over Shape {
    pub self () {}
    
    impl Printable; // reimplement the method print with typeof (self) being main::Rectangle
}

def main () {
    let c = Circle::new ();
    c.print ();
    
    let r = Rectangle::new ();
    r.print ();
}


Results:

main::Shape
main::Rectangle

Override implemented method

Implemented method cannot be overriden without reimplementing the trait. In the following example, a class Shape implement the trait Printable, and the class Circle inherits Shape, and tries to override the method print.

mod main;
import std::io;

trait Printable {
    pub def print (self);
}

class @abstract Shape {
    self () {}
    
    impl Printable {
        pub over print (self) {
            println ("main::Shape");
        }
    }
}

class Circle over Shape {
    pub self () {}
    
    pub over print (self) {}
}


Errors:

Error : when validating main::Circle
 --> main.yr:(18,7)
18  ┃ class Circle over Shape {
    ╋       ^^^^^^
    ┃ Error : cannot override the trait method (const self) => main::Shape::print ()-> void outside the implementation of the trait
    ┃  --> main.yr:(21,14)
    ┃ 21  ┃     pub over print (self) {}
    ┃     ╋              ^^^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(12,18)
    ┃     ┃ 12  ┃         pub over print (self) {
    ┃     ┃     ╋                  ^^^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


To prevent the previous error, the class Circle have to reimplement the trait Printable. When reimplementing a trait in a heir class, the parent overriding is not taken into account, and the method of the trait is used. In the following example, the class Shape implement the trait Printable, that have a method print with no default behavior. The class Circle tries to reimplement the trait, but without overriding the print method. This source code is rejected by the compiler, the class Circle is not abstract, but has a method with no body.

mod main;
import std::io;

trait Printable {
    pub def print (self);
}

class @abstract Shape {
    self () {}
    
    impl Printable {
        pub over print (self) {
            println ("main::Shape");
        }
    }
}

class Circle over Shape {
    pub self () {}
    
    impl Printable;
}


Errors:

Error : when validating main::Circle
 --> main.yr:(18,7)
18  ┃ class Circle over Shape {
    ╋       ^^^^^^
    ┃ Error : the class main::Circle is not abstract, but does not override the empty parent method (const self) => main::Printable::print ()-> void
    ┃  --> main.yr:(18,7)
    ┃ 18  ┃ class Circle over Shape {
    ┃     ╋       ^^^^^^
    ┃     ┃ Note : 
    ┃     ┃  --> main.yr:(5,10)
    ┃     ┃  5  ┃ 	pub def print (self);
    ┃     ┃     ╋ 	        ^^^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


To resolve that problem, the class Circle must add a body to the method print. It can happen that a trait defines multiple methods, and that only some have to be reimplemented by the heir class. In that case, there is no magical solution, maybe a contribution can enhance that, but every methods must be reimplemented. In order to mimic the behavior of the parent implementation, the super keyword can be used.

class Circle over Shape {
    pub self () {}	

    impl Printable {
    
        pub over print (self)-> void { 
            self::super.print (); // call the print method of the super class 		
        }
    }	
}


Trait privacy

Trait implementation is always public. For that reason, privacy specifier (pub, prot and prv) have no meaning on implementation.

On the other hand, the trait methods implementation follows the same rule as the overriding of a parent method. That is to say, the privacy defined inside the trait must be the same as the privacy defined inside the implementation.

Trait usage

As said in the beginning of this chapter, traits do not define types, thus they cannot be used to define the type of a variable. For example, the following source code has no meaning, Printable does not define a type.

mod main;
import std::io;

trait Printable {
    pub def print (self);
}

def foo  (a : Printable) {
    a.print ();
}


Errors:

Error : expression used as a type
 --> main.yr:(8,15)
 8  ┃ def foo  (a : Printable) {
    ╋               ^^^^^^^^^


ymir1: fatal error: 
compilation terminated.


If the previous example, is not a valid ymir code, the behavior can still be implemented in the language. Traits gain interest when coupled with templates, and a template test can be used to check that a class implement a trait. More complete information, and example about templates, and traits specialization are presented in chapter Templates, but a brief example is presented in the following source code. In this example, two classes U and V implement the trait Printable. The function foo takes a parameter whose type is not specified but must implement the trait Printable. Thus the function is callable with both U or V as argument.

mod main;
import std::io;

trait Printable {
    pub def print (self) {
        println (typeof (self)::typeid);
    }
}

class U {
    pub self () {}
    impl Printable;
}

class V {
    pub self () {}
    impl Printable;
}

/**
 * Accept every type, that implements the trait Printable 
 */
def foo {I impl Printable} (a : I) {
    a.print ();
}

def main () {
    foo (U::new ());
    foo (V::new ());
}


Results:

main::U
main::V

Cast, and dynamic typing

An object instance of a heir class can be casted to an object instance of an ancestor class. Unlike, casting of integer values, (e.g. i32 to i64), because an object is an aliasable type, the memory size of the object is not modified. Casting must respect mutability of the object value. Moreover, this cast can be made implicitely, as it does not create any problem in memory. In the following example, the class Bar inherits from the class Foo. A variable x is created, and is of type &Bar. At line 1, an implicit cast is made of a &Bar value to a &Foo value, the same cast is made but explicitely at line 1.

import std::io;

class Foo {
    pub self () {}
    
    pub def foo (self) {
        println ("Foo");
    }
}

class Bar over Foo {
    pub self () {}
    
    pub over foo (self) {
        println ("Bar");
    }
}

def baz (f : &Foo) {
    f.foo ();
}

def main () {
    let x = Bar::new ();
    baz (x);
    
    let y = cast!{&Foo} (x);
    y.foo ();
}


Results:

Bar
Bar

Dynamic typeinfo

One can note from the above example, that when a variable contains a value of type &Foo, that does not necessarily mean that this is a pure &Foo value, but it can be a &Bar. In object oriented programming, this principle is denote polyphormism. In the chapter Basic programming concepts, we have seen that every object has specific attributes. Object is no exception to the rule. The following table lists the default specific attributes of the object types.

Name Meaning
typeid The name of the type stored in a value of type [c32]
typeinfo A structure of type TypeInfo, containing information about the type

These attributes are compile time executed, and thus are static. For example, in the following source code, the typeid of the class Bar is printed to stdout, followed by a line that does exactly the same thing (literraly).

def main () {
    println (Bar::typeid);
    println ("main::Bar");
}


When it comes to dynamic typing, it can be interesting to get the typeinfo of the type of the value that is actually stored inside the variable (e.g. get the typeinfo of the Bar class, type of the value contained inside a &Foo variable). To do that, the specific attribute typeinfo of object is also accessible from the value directly, and this time dynamically.

def main () {
    let x = Bar::new ();
    let y : &Foo = x;
    
    println (y::typeinfo);
}


Results:

core::typeinfo::TypeInfo(13, 16, [core::typeinfo::TypeInfo(13, 16, [], main::Foo)], main::Bar)


The result presented above, gives the following information : 1) we have a object, 2) its size is 16 bytes, 3) it has an ancestor (object, size 16 bytes, no ancestor, named main::Foo), 4) its name is main::Bar.

The TypeInfo returned by the typeinfo attributes (either dynamically or statically), is a structure whose definition is the following. The inner fields depend on the value of the typeid field, for example, when dealing with an object it stores the ancestor TypeInfo, and when dealing with slice, it stores the TypeInfo of the type contained inside the slice.

pub struct
| typeid : TypeIDs
| size   : usize
| inner  : [TypeInfo]
| name   : [c32] 
 -> TypeInfo;

pub enum : u32
| ARRAY        = 1u32
| BOOL         = 2u32
| CHAR         = 3u32
| CLOSURE      = 4u32
| FLOAT        = 5u32
| FUNC_PTR     = 6u32
| SIGNED_INT   = 7u32
| UNSIGNED_INT = 8u32
| POINTER      = 9u32
| SLICE        = 10u32
| STRUCT       = 11u32
| TUPLE        = 12u32
| OBJECT       = 13u32
| VOID         = 14u32
 -> TypeIDs;


The typeinfo of a class is stored in the text and is accessible from the vtable of the object. One can note that Bar and Foo have a size of 16 bytes, despite the fact that they store no fields. This is due to two pointers that are stored inside every objects, the first pointer is refering to the monitor of the object (cf. Parallelism), and the second one points the the vtable of the object.

Object

Ymir have a type named Object, that can used to cast any object into that type. The reverse is impossible. We have seen that the object are not inheriting from a global ancestor, and this is really not the case. This cast unlike casting to parent class objects, cannot be made implicitely. We can see the &(Object) type as the &(void) type, that can store any pointer, but for objects. Unlike &(void) (in which by the way we can't cast objects), &Object stores one valuable information, it stores a valid object value, with a vtable, a monitor, a typeinfo, and cannot be null.

In the following example, a pattern matching is used to check the type of the object that is returned by the foo function. This is discussed in chapter Pattern Matching.

def foo ()-> &Object {
    cast!{&Object} (Foo::new ())
}

def main () {
    match foo () {
        Foo () => {
            println ("I got a Foo !");
        }
    }
}


Results:

I got a Foo !


Some function of the standard library uses the Object type to return values, when it is impossible statically to get more accurate information about the type (e.g. [Packable] (https://gnu-ymir.github.io/Documentations/en/traits/serialize.html).)

Functions

We have seen in the chapter Basic programming concepts how functions are written. Ymir can be used as a functional language, thus functions can also be considered as values. In this chapter we will see more advanced function systems, named function pointer and closure.

Function pointer

A function pointer is a value that contains a function. It can be used for example, to pass a function, as an argument to other functions. The type of a function pointer is written using the keyword fn, and have nearly the same syntax as a function prototype, but without a name, and without naming the parameters.

import std::io

def foo (f : fn (i32)-> i32) -> i32 {
    f (41)
}

def addOne (x : i32)-> i32
    x + 1


def main () {
    let x = foo (&addOne);
    println (x);
}


In the above example, we have specified that the function foo takes a function pointer as first parameter. This function pointer, is a function that takes an i32 value and return another i32 value. In the main function, the ampersand (&) unary operator is used to transform the function symbol addOne into a function pointer. This function pointer is then passed to the function foo, which calls it and return its value.

Results:

42

Function pointer using reference

We have seen that references is not a type, in the chapter Alias and References. However, function prototype sometimes takes reference value as parameter. This must be replicated in the prototype of the function pointer. For that reason, the ref keyword can be used in the prototype of a function pointer type.

In the following example, the function mutAddOne change the value of a reference variable x, and add one to it. The function foo takes a function pointer as first parameter, and calls it on a mutable local variable x by reference (it is important).

import std::io

def foo (f : fn (ref mut i32)-> void) -> i32 {
    let mut x = 41;
    f (ref x);
    x
}

def mutAddOne (ref mut x : i32) {
    x = x + 1;
}

def main () {
    let x = foo (&mutAddOne);
    println (x);
}


Results:

42


The prototype of the function pointer must be strictly respected, for obvious reasons. And as for normal functions, alias and references must be strictly respected as well. For example, in the follow example, the function foo tries to call the function pointer that takes a reference argument, using a simple value. And the main function tries to call the foo function, with a function pointer that does not take a reference parameter.

import std::io

def foo (f : fn (ref mut i32)-> void) -> i32 {
    let mut x = 41;
    f (x);
    x
}

def mutAddOne (x : i32) {
    println (x);
}

def main () {
    let x = foo (&mutAddOne);
    println (x);
}


We have two errors, first the compiler does not allow an implicit referencing of the variable x at line 5, and second the compiler does not allow an implicit cast of a value of type fn (i32)-> void to fn (ref mut i32)-> void.

Error : the call operator is not defined for &fn(ref mut i32)-> void and {mut i32}
 --> main.yr:(5,7)
 5  ┃     f (x);
    ╋       ^ ^
    ┃ Error : implicit referencing of type mut i32 is not allowed
    ┃  --> main.yr:(5,8)
    ┃  5  ┃     f (x);
    ┃     ╋        ^
    ┃ Note : for parameter i32 --> main.yr:(3,26) of f
    ┗━━━━━━ 

Error : the call operator is not defined for main::foo and {&fn(i32)-> void}
 --> main.yr:(14,17)
14  ┃     let x = foo (&mutAddOne);
    ╋                 ^          ^
    ┃ Note : candidate foo --> main.yr:(3,5) : main::foo (f : &fn(ref mut i32)-> void)-> i32
    ┃     ┃ Error : incompatible types &fn(ref mut i32)-> void and &fn(i32)-> void
    ┃     ┃  --> main.yr:(14,18)
    ┃     ┃ 14  ┃     let x = foo (&mutAddOne);
    ┃     ┃     ╋                  ^
    ┃     ┃ Note : for parameter f --> main.yr:(3,10) of main::foo (f : &fn(ref mut i32)-> void)-> i32
    ┃     ┗━━━━━━ 
    ┗━━━━━┻━ 

Error : undefined symbol x
 --> main.yr:(15,14)
15  ┃     println (x);
    ╋              ^


ymir1: fatal error: 
compilation terminated.

Lambda function

Lambda functions are anonymous functions that have the same behavior as normal function, but don't have a name. They are declared using the token | surrounding the parameters instead of parentheses in order to dinstinguish them from tuple. The following code block presents the syntax of the lambda functions.

lambda_func := '|' (var_decl (',' var_decl)*)? '|' ('->' type)? ('=>')? expression
var_decl := Identifier (':' type)?


The following example shows a simple usage of a lambda function. This function declared at line 4, and stored in the variable x, takes two parameters x and y of type i32, and return their sum.

import std::io

def main () {
    let x = |x : i32, y : i32|-> i32 { 
        x + y
    };
    println (x (1, 2));
}


As one can note, there is no conflict between the variable x declared in the function main, and the first parameter of the lambda function also named x. This is due to the fact that the lambda function does not enclose the context of the function that have created it. In other words, lambda functions behave as normal local function, accessible only inside the function that have declared them (cf. Scope declaration).

In many cases the type of the parameters and return type can be infered, and are therefore optional. The above example can then be rewritten into the following example. In this next example, the lambda function can be called with any values, as long as the binary addition (+) operator is defined between the two values.

import std::io

def main () {
    let x = |x, y| x + y;	
    println (x (1, 2));
    
        
    // The types are not given, then you can also write 
    println (x (1.3, 2.9));	
}


The token => can be added after the prototype of the lambda, to make it a bit more readable. It is just syntaxic and has no impact on the behavior of the lambda.

import std::io

def main () {
    let x = |x, y| => x + y;	
    println (x (1, 2));
}


Lambda functions are directly function pointers, and then can be used as such without needing the unary ampersand (&) operator. In the following example, the function foo takes a function pointer as first parameter, and two i32 values as second and third parameters. This function calls the function pointer twice, and add the result. A lambda function is used in the main function as the first argument for the foo function.

import std::io

def foo (f : fn (i32, i32)-> i32, x : i32, y : i32) -> i32 {
    f (x, y) + f (y, x)
}

def main () {
    let x = (|x, y|=> x * y).foo (3, 7);
    
    // uniform call syntax is used, but you can of course write it as follows : 
    // foo (|x, y| x*y, 3, 7);
    println (x);
}

Results:

42


Lambda function that are not typed are special element, that does not really have a value at runtime, and are closer to compile time values (presented in a future chapter Compile time execution). When the whole type of a lambda cannot be infered by the compiler (types of the parameters, and the type of the return type), then the value cannot be passed to a mutable variable. Ymir allows to put an untyped lambda inside an immutable var, to ease its usage, but the lambda still does not have any value. For that reason, the second line of the following example is possible, but not the third.

def main () {
    let x = |x| x + 1;
    let mut y = x;
}


Errors:

Error : the type mut fn (any)-> any is not complete
 --> main.yr:(2,10)
 2  ┃ 	let x = |x| x + 1;
    ╋ 	        ^
    ┃ Note : 
    ┃  --> main.yr:(3,10)
    ┃  3  ┃ 	let mut y = x;
    ┃     ╋ 	        ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


The same problem happens when an uncomplete lambda function is used as the value of a function. To resolve the problem, and because the return type of a function is always complete when the function is validated (or there were other previous errors), the keyword return can be used. Thanks to that statement, the compiler has additional knowledge, and can infer the type of the lambda function from the return type of the function.

import std::io

def foo () -> fn (i32)-> i32 {
    return |x| => x + 12 // the compiler tries to transform the lambda function into a function pointer fn (i32)-> i32
}

def main () {
    let x = foo ();
    println (x (30));
}


Contribution: Resolve that problem when it seems obvious, for example in the previous example, maybe the type of the block can be infered directly?

Closure

As said earlier, a lambda function behave like a local private function, and thus has no access to the context of the function that have declared it. In the following example, the lambda function declared at line 5 tries to access the variable i declared at line 4. This is impossible, the variable i exists in a different context that the lambda function.

import std::io

def main () {
    let i = 12;
    let x = | | {
        println (i);
    };
    x ();
}


Errors:

Error : undefined symbol i
 --> main.yr:(6,12)
 6  ┃ 		println (i);
    ╋ 		         ^

Error : undefined symbol x
 --> main.yr:(8,2)
 8  ┃ 	x ();
    ╋ 	^


ymir1: fatal error: 
compilation terminated.


Closure are a function pointer that capture the environment of the function that has declared them. In Ymir there is only one kind of accepted closure, that is called the move closure.

Copy closure

A copy closure is a special kind of lambda function, that is declared by using the keyword move in front of a lambda literal. The closure as an immutable access to all the variable declared inside the scope of the parent function. This closure is called a copy closure because the access of the variable is made by copy (a first level copy cf. Copy and Deep copy). Because closure captures a context in addition to a function pointer, the simple function pointer type is no more sufficient, and a new type is introduced. The syntax of the closure type is created with the keyword dg instead of fn (dg stands for delegate). A delegate is a function pointer with an environment, and is the general case of a closure (we will see in next section, a case of delegate that are not closure).

In the following example, the copy closure declared at line 9 enclosed the scope of the function foo, and thus has access to the variable i. However, the enclosed variable is immutable (and is a copy).

import std::io

def bar (f : dg (i32)-> i32) -> i32 {
    f (12)
}

def foo () {
    let i = 30;
    let x = bar (move |x|=> x + i);
    println (x);
}

def main () {
    foo ();
}


The above source code in the context of the foo function, can be illustrated by the following figure.


<img src="https://gnu-ymir.github.io/Documentations/en/functions/closure.png" alt="drawing" height="500", style="display: block; margin-left: auto; margin-right: auto;">


As one can note, the variable i enclosed in the closure is not the same as the variable i of the main function. This has two impact:

  • a) copy closure can be returned safely from functions, indeed even when the variable i does not exist anymore as the function foo is exited, a copy of it is still accessible in the heap (note that this is the same for aliasable types, that are in the heap in any case). For example:
import std::io;

def foo ()-> dg ()-> i32 {
    let i = 30;
    return (move || => i + 12);
}

def main () {
    let x = foo ();
    println (x ()); // enclosed i does not exists, but thats not a problem
}


Results:

42


  • b) the value of the enclosed i is independant from the value of the variable i in the foo function, meaning that there is no way for the foo function to change the value of the variable i inside the closure after its creation. For example :
import std::io;

def main () {
    let mut i = 30;
    let x = move || => i + 12;
    i = 11; // no impact on the closure of x
    println (x ());    
    
    let y = move || => i + 12;
    println (y ());
}


Results:

42
23


By using aliasable types, this limitation can be bypassed, for example a slice can be used to enclosed the value of i, and access it from the closure, without removing the guarantees of the copy closure, this is illustrated in the following example. Warning: if you might be tempted to use a pointer on the i variable, its highly not recommended. Indeed, pointing to a local variable remove the guarantee we introduced earlier in the point (a) - (in general using pointer - not function pointer - to value is a bad idea, and should be prohibited outside the std).

import std::io;

def main ()
    throws &OutOfArray
{
    let dmut i = [12];
    let x = move || => {
        i[0] + 12
    } catch {
        _ => {
            0
        }
    };
        
    i [0] = 30;
    println (x ());
}


In the above example, the copy closure access to the first index of the slice i. This is a unsafe operation, the slice can be empty, this is why a catch is made. Information about catch is not presented here, and will be discussed in a future chapter Error handling. Here because the slice is not empty when the closure is called, the access works.

Results:

42

Method delegate

A method is a function pointer associated with a object instance, then they can be seen as delegate. The name closure is not used here, because nothing is really enclosed as in copy closure over function context, so the name delegate being a more global term is used. A delegate is a function operating on an object, for which we don't know the exact type.

A method can be transformed into a delegate using the unary ampersand (&) operator, on a method associated to an object instance.

import std::io;

class Foo {
    pub let mut i = 0;
    
    pub self () {}
    
    pub def foo (self) -> void {
        println (self.i);
    }
}

def main () {
    let dmut a = Foo::new (), dmut b = Foo::new ();
    let x : (dg ()-> void) = &a.foo;
    let y = &b.foo;
    
    a.i = 89;
    b.i = 42;
    
    x ();
    y ();	
}


Results:

89
42


Unlike copy closure a method can have a mutable access to the object associated to it. In that case, an explicit alias must be made on the object instance, when creating the delegate, otherwise the compiler throws an error.

import std::io;

class Foo {
    let mut _i = 0;
    
    pub self () {}
    
    pub def foo (mut self) {
        self._i = 42;
    }

    impl Streamable;
}

def main () {
    let dmut a = Foo::new ();

    let x = &(a.foo);

    x ();

    println (a);

}


Errors:

Error : undefined operator & for type (a).foo
 --> main.yr:(18,13)
18  ┃     let x = &(a.foo);
    ╋             ^
    ┃ Note : candidate foo --> main.yr:(8,13) : (mut self) => main::Foo::foo ()-> void
    ┃     ┃ Error : discard the constant qualifier is prohibited, left operand mutability level is 2 but must be at most 1
    ┃     ┃  --> main.yr:(18,13)
    ┃     ┃ 18  ┃     let x = &(a.foo);
    ┃     ┃     ╋             ^
    ┃     ┃     ┃ Note : implicit alias of type mut &(mut main::Foo) is not allowed, it will implicitly discard constant qualifier
    ┃     ┃     ┃  --> main.yr:(18,15)
    ┃     ┃     ┃ 18  ┃     let x = &(a.foo);
    ┃     ┃     ┃     ╋               ^
    ┃     ┃     ┗━━━━━┻━ 
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 

Error : undefined symbol x
 --> main.yr:(20,5)
20  ┃     x ();
    ╋     ^


ymir1: fatal error: 
compilation terminated.


This can be easily resolved by aliasing the variable a when creating the delegate. Either by using the keyword alias, or by using the :. binary operator.

import std::io;

class Foo {
    let mut _i = 0;
    
    pub self () {}
    
    pub def foo (mut self) {
        self._i = 42;
    }

    impl Streamable; // to make the type printable
}

def main () {
    let dmut a = Foo::new ();
    let x = &(a:.foo); // or &((alias a).foo);

    x ();

    println (a);
}


Results:

main::Foo(42)


Polymorphic delegate

Method delegates respect the polyphormism introduced by class inheritance.

import std::io;

class Foo {
    pub self () {}
    
    pub def foo (self) {
        println ("Bar");
    }
}

class Bar over Foo {
    pub self () {}

    pub over foo (self) {
        println ("Bar");
    }
}


def main ()
{
    let x : &Foo = Bar::new ();
    let d = &(x.foo);
    d ();
}


Results:

Bar

Pattern matching

The pattern matching is an important part of the Ymir language. It allows to make test over values and moreover on types, especially when it comes to objects. The pattern matching syntax always start with the keyword match followed by an expression, and then a list of patterns enclosed between {}.

The syntax of the pattern matching is described in the following code block.

match := 'match' expression '{' pattern* '}'
pattern := pattern_expression '=>' expression (';')?

pattern_expression :=   pattern_tuple 
                      | pattern_option 
                      | pattern_range 
                      | pattern_var 
                      | pattern_call
                      | expression
                      
pattern_tuple := '(' (pattern_expression (',' pattern_expression)*)? ')'
pattern_option := pattern_expression ('|' pattern_expression)*
pattern_range := pattern_expression ('..' | '...') pattern_expression 
pattern_var := (Identifier | '_') ':' (type | '_') ('=' pattern_expression)?
pattern_call := (type | '_') '(' (pattern_argument (',' pattern_argument)*)? ')'
pattern_arguments := (Identifier '->')? pattern_expression


Match is a kind of control flow, relatively close to if expressions. As if expressions, a match expression can have a value. In that case, every branch of the match must share the same type, and there must be a guarantee that at least one test of the match succeed, and thus that a branch is entered. For example, in the following example, all the branch of the match share the same type i32, however it is possible (and even inevitable in that specific case), that no branch of the match were entered. So the compiler throws an error, as the variable x might be unset, which is prohibited by the language. In the following example, simple tests are made on the value, so the first pattern is equal to an if expression, where the test is 12 == 1.

def main () {
    let x = match (12) {
        1 => 8
        2 => 7
        3 => 6
    };
}


Errors :

Error : match of type i32 has no default match case
 --> main.yr:(2,10)
 2  ┃ 	let x = match (12) {
    ╋ 	        ^^^^^


ymir1: fatal error: 
compilation terminated.

Matching on everything

The token _ declares a pattern test that is always valid. It can be placed at different level of the pattern test, as we will see in the rest of this chapter.

import std::io;

def main () {
    match 42 {
        _ => { println ("Always true"); }
    }
}


Matching over a range of values

Pattern matching aims to be more expressive than if expressions, and therefore to allow faster writting of complex test. For example, to check wether a value is included in an interval of values, writing the interval in the test of the pattern is sufficient. In the following example, the first pattern can be rewritten as the following if expression : 1 <= 42 && 10 > 42, the second into : 10 <= 42 && 40 >= 42, and the third one into : 42 == 41 || 42 == 42 || 42 == 43.

import std::io

def main () {
    match 42 {
        1 .. 10 => {
            println ("The answer is between 1 and 10 not included");
        }
        10 ... 40 {
            println ("The answer is between 10 and 40 included");
        }
        41 | 42 | 43 => {
            println ("The answer is either 41, 42 or 43");
        }
    }
}


The pattern are tested in the order they are written in the source code, thus if two pattern are valid, only the first one is entered. For example, in the following source code, only the pattern at line 5 is entered, and the pattern at line 6, even if it is valid, is simply ignored.

import std::io;

def main () {
    match 42 {
        1 .. 100 => { println ("Between 1 and 100"); }
        10 .. 100 => { println ("Between 10 and 100"); }
    }
}

Variable pattern

A variable declaration can be used to store a value during the pattern matching. The variable is declared like a standard variable declaration but with keyword let ommitted. The variable pattern can also be used to match over the type of the expression that is tested, when the type of the variable can be dynamic (e.g. on class inheritance). In all other cases the test is done during the compilation, and the type of the newly declared variable must in any case be fully compatible with the type of the value that is tested. In the following example, the type of the variable patterns is always i32, because it is the only compatible type with the type of the value.

import std::io

def main () {
    match 13 {
        _ : i32 => {
            println ("It is a i32, but I don't care about the value");
        }
        _ : i32 = 13 => {
            println ("It is a i32, whose value is 13");
        }
        _ : _ = 13 => {
            println ("It is a i32, whose value is 13, but I didn't checked the type");
        }		
        x : i32 => {
            println ("It is a i32, and the value is : ", x);
        }
    }
}


In the above example, every pattern tests are valid, but only the first pattern is evaluated, leading to the following result.

It is a i32, but I don't care about the value


One can note that the token : is important in that case, even if the type is not mandatory and can be omitted (by replacing it with the token _). This is to distinguish a variable declaration to a simple variable referencing. For example, in the following source code, a variable x is declared before the pattern matching, and its value is compared with the value that is tested in the match. A second pattern declares a variable y.

import std::io;

def main () {
    let x = 42;
    match 42 {
        x => { println ("x == 42"); } // simple test on the variable declare 2 lines above
        y : _ => { println (y); } // declaration of a new variable y that stores the matched value 
    }
    
}


Results:

x == 42


Matching over type

When the type of the value that is tested can be dynamic (i.e. class inheritance, which is the only possibility), then the type of the variable in the test can be used to test the type of the value. In the following example, the class Bar and Baz inherit from the abstract class Foo. The variable x declared in the main function, is of type Foo meaning that it can contains either a Bar or a Baz value. The match expression then make a test over the type of the variable x.

import std::io;

class @abstract Foo {
    self () {}
}

class Bar over Foo {
    pub self () {}
}

class Baz over Foo {
    pub self () {}
}

def foo ()-> &Foo {
    Bar::new ()
}

def main () {
    let x = foo ();
    match x {
        _ : &Bar => println ("Contains a Bar");
        _ : &Baz => println ("Contains a Baz");		
    }
}


Results:

Contains a Bar


There is another pattern that can be used to test a dynamic type, that is presented in a following sub section (cf. Destructuring class), but pattern matching is the only way to cast a value whose type is an ancestor class to an heir class, and this way is safe. In many language like Java, D or C++, it is possible to use the casting system, that has a undefined behavior in C++, makes the program crash in Java, and returns the value null in D. These three behaviors are not acceptable since they are not safe. By using the pattern matching, the failing case is let to the discretion of the user. And as we have seen in the introduction of this chapter, because a match can't have a value if there is a possibility that none of the branch were entered, then the user has to write a default case when the cast failed if they want to retreive a value from the matching. This default case can of course be used to throw an exception (cf Error handling).

Reference variable

A mutable value can be updated inside a pattern, by using a reference variable. This works exactly like variable referencing (as presented in chapter References).

import std::io

def main () {
    let mut z = 1;
    match ref z {
        // ^^^
        // ref is important here, otherwise the compiler throw an error
        ref mut x : _ => {
            x = 42;
        }
    }
    
    println (z);
}

Destructuring patterns

Destructuring patterns are patterns that divide the values contained in a value is type is a compound type. Compound types are 1) tuple, 2) structures and 3) classes.

Destructuring a tuple

To destructure a tuple, parentheses surrounding other patterns are used. The arity of the destructuring pattern must be the same as the arity of the tuple that is destructured. In the following example, a tuple of arity 3, and type (i32, c32, f64) is destructured, using two different patterns. The first pattern only check the first value of the tuple (the other are always true, using the token _), by verifying that the value is equals to 1 and putting it in the variable i. The second pattern does not do any tests but associate the values of the tuple to the variable x, y and z. As one can note from that test, any pattern can be used to test inner values of the tuple, (another destructuring pattern if the inner value is a compound type, a range pattern, etc.)

import std::io

def main () {
    let tuple = (1, 'x', 3.14)
    
    match tuple {
        (i : i32 = 1, _, _) => {
            println ("This tuple has an arity of three and its first element is an i32, whose value ", i, " == 1");
        } 
        (x : _, y : _, z : _) => {
            println ("This tuple has an arity of three, and its values are : (", x, ", ", y, ", ", z, ")");
        }		
    }
}


Results:

This tuple has an arity of three and its first element is an i32, whose value 1 == 1

Destructuring structure

Destructuring structure is made by using a call expression. The argument of the call expressions are patterns. Unlike tuple destructuring, there is no need to test all the values of the structure, but only those which are relevant. The order of the fields is respected in the destructuring (i.e. the pattern, at line 15 of the following example, tests the value of the field x). Named expressions can be used to test specific fields of the structure.

import std::io

struct
| x : i32
| y : i32
 -> Point;
    
def main () {
    let p = Point (1, 2);
    
    match p {
        Point (y-> 1) => {
            println ("Point where y is equal to 1");
        }
        Point (1) => {
            println ("Point where x is equal to 1");
        }
        Point () => {
            println ("Any point");
        }
    }
}


Of course, any kind of pattern can be used inside a structure destructuring, for example a variable pattern, that refers to the values later in the content of the pattern. In the following example, a variable p is declared to refer to the value contained in the variable point, and a variable y is declared to refer to the value contained in the field point.y.

import std::io

struct
| x : i32
| y : i32
 -> Point;
    
def main () {
    let point = Point (1, 2);
    
    match point {
        p : _ = Point (y-> y : i32) => {
            println (p, " is a point, whose y field is equal to ", y);
        }
    }
}

Destructuring class

The syntax for destructuring object is the same as the syntax for destructuring structure. However, only named expressions are admitted, and this expressions refer to object fields that must be public in the context of the pattern matching. For example, in the source code below, the field x of the class Point is public from the context of the main function, for that reason it is accessible inside the class destructuring pattern. On the other hand the field _y is private for the main function, thus cannot be used.

import std::io

class Point {
    pub let x : i32 = 1;
    let _y : i32 = 2;
    
    pub self () {}
}

def main () {
    let p = Point::new ();
    
    match p {
        Point (x-> 1, _y-> 2) => {
            println (p.x, " is a equal to 1");
        }
    }
}


Errors:

Error : undefined field _y for element &(main::Point)
 --> main.yr:(14,17)
14  ┃ 		Point (x-> 1, _y-> 2) => {
    ╋ 		              ^
    ┃ Note : i32 --> main.yr:(5,11) : _y is private within this context
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.


We have seen in a previous section (cf. Matching over type), that because class types are dynamic when there is inheritance, pattern matching can be used to test the type of the values. Class destructuring is an alternative way to check the type of a value, whose type is a class that have heirs. The following example, demonstrate the use of the pattern matching to retreive the center of a Shape when it is a Circle. In this example, the foo function lied, and returned a Rectangle instead of a Circle, in order to be compilable the source code must manage that case, otherwise the variable center declared at line 28 could be unset, and that is prohibited by the language.

import std::io

class @abstract Shape {
    self () {}
}

class Rectangle over Shape {
    pub self () {}
}

class Circle over Shape {
    pub let center : i32 ;

    pub self (center : i32) with center = center {}
}

/**
 * Don't worry I will return you a circle
*/
def foo () -> &Shape {
    Rectangle::new ()
}

def main () {
    let circle = foo ();
    let center = match circle {
        Circle (center-> c : _) => c
    };
    println ("The center of the circle is : ", center);
}


Errors:

Error : match of type i32 has no default match case
 --> main.yr:(28,15)
28  ┃ 	let center = match circle {
    ╋ 	             ^^^^^

Error : undefined symbol center
 --> main.yr:(31,45)
31  ┃ 	println ("The center of the circle is : ", center);
    ╋ 	                                           ^^^^^^


ymir1: fatal error: 
compilation terminated.


This can be corrected by adding a default case to the match expression. The following source codes are two possibilities.

    1. Setting a default value :
def main () {
    let circle = foo ();
    let center = match circle {
        Circle (center-> c : _) => c
        _ => {
            println ("Foo lied ....");
            0 // center is equal to 0, if foo returned something other than a Circle
        }
    };
    println ("The center of the circle is : ", center);
}
def main ()
    throws &AssertError
{
    let circle = foo ();
    let center = match circle {
        Circle (center-> c : _) => c
        _ => {
            throw AssertError::new ("Foo lied...");
        }
    };
    println ("The center of the circle is : ", center);
}


A pattern using an ancestor class, will succeed if the object instance that is used is a heir class. That is to say, if the pattern tries to get a Shape value, when giving a Circle value to the pattern, the pattern test succeeds. So the order has to be carefully set (putting heir class tests first). The phenomenon is the same with variable patterns. Contribution Add verification when a pattern test cannot be entered because previous test is always valid.

def main () {
    let circle : &Shape = Circle::new (); // Important to have a &Shape, and not a &Circle
    match circle {
        Shape () => {
            println ("Shape");
        }
        Circle () => {
            println ("Circle");
        }
    }
}


Results:

Shape

Error handling

This section will introduce error handling in the Ymir language. As with many languages, error are managed by throwing exceptions. Exception can be recovered thanks to scope guards that manage the errors in different manners. The keyword throw is used to throw an exception when an error occurs in the program. An exception is a class that inherits the core class core::exception::Exception. Exceptions are always recoverable, and must be managed by the user, who cannot simply ignore them. Ymir does not allow the possibility to ignore that an exception is thrown in a function, and may cause the function to exit prematurely. To avoid this possibility, excpetion must be written in the definition of the function, or managed directly.

// Exception is defined in a core module, so does not need import 
class MyError over Exception {
    pub self () {}
}

def main () 
    throws &MyError
{
    throw MyError::new ();
}

Assert

We have seen in previous section the assert expression. This simple expression throws an &AssertError value when the condition is not valid. AssertError is a common exception defined in a core file (that does not need to be imported).

def main () 
    throws &AssertError
{
    let i = 11;
    assert (i < 10, "i must be lower than 10")
}

Rethrowing

The error rethrowing is a way of defining that a function could throw an exception, and that this exception must be taking into account by the caller functions. It is a system relatively close to error rethrowing of the Java language, apart that the specific name of the exception must be written in the possible rethrowed exceptions. That is to say, it is impossible to write that a function throws a parent class of the actually thrown exception (e.g. &Exception, when the function actually throws &AssertError). Thanks to that, the compiler is always able to check the type of the exceptions, and can force the user to handle them.

In the following example, the function foo is an unsafe function that can throw the exception &AssertError. This exception is thrown by the function assert at line 6, and is not managed by the function foo. Because the main function calls the foo function, it is also unsafe, and also throws the exception &AssertError. In this example, the program stops, because of an unhandled exception.

import std::io

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i is not lower than ten");
    println (i);
}

def main () 
    throws &AssertError
{
    foo (10);
}


Results (in debug mode, -g option):

Unhandled exception
Exception in file "/home/emile/ymir/Runtime/midgard/core/exception.yr", at line 84, in function "core::exception::abort", of type core::exception::AssertError.
╭  Stack trace :
╞═ bt ╕ #1
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3 in function core::exception::abort (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:84
╞═ bt ╕ #4 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:7
╞═ bt ╕ #5 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #6
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #7 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #8
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #9 in function _start
╰
Aborted (core dumped)


The compiler does not allow to forget the possibility of a error throwing, and requires the user to write it down. In the following example, the function foo call the function assert that could throw an &AssertError if the test fails. In that case the function foo can also throw an error, and that must be written in the prototype of the function. Otherwise the compiler gives an error.

import std::io

def foo (i : i32) 
{
    assert (i < 10, "i is not lower than ten");
    println (i);
}


Errors:

Error : the function main::foo might throw an exception of type &(core::exception::AssertError), but that is not declared in its prototype
 --> main.yr:(3,5)
 3  ┃ def foo (i : i32) 
    ╋     ^^^
    ┃ Note : 
    ┃  --> main.yr:(5,2)
    ┃  5  ┃ 	assert (i < 10, "i is not lower than ten");
    ┃     ╋ 	^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


As previously stated, the name of the exceptions specified in the prototype function must be the actual name of the exception, not the name of an ancestor. In the following example, the class ParentException and ChildException are two throwable class. The function foo throws an object of type ChildException, but the prototype declares that the function throws a ParentException object. To avoid losing accuracy, the Ymir language does not allow that. This is however still possible to perform this kind of behavior (necessary when the function throw multiple kind of errors, all deriving from ParentException for example), by using a cast, that we have seen in chapter Cast and Dynamic typing. That way, there is a loss of accuracy, but properly defined and intended by the user.


class ParentException over Exception {
    pub self () {}
}

class ChildException over Exception {
    pub self () {}
}

def foo () 
    throws &ParentException
{
    throw ChildException::new ()
}


Errors:

Error : the function main::foo might throw an exception of type &(main::ChildException), but that is not declared in its prototype
 --> main.yr:(9,5)
 9  ┃ def foo () 
    ╋     ^^^
    ┃ Note : 
    ┃  --> main.yr:(12,5)
    ┃ 12  ┃     throw ChildException::new ()
    ┃     ╋     ^^^^^
    ┗━━━━━┻━ 

Error : the function main::foo prototype informs about a possible throw of an exception of type &(main::ParentException), but this is not true
 --> main.yr:(9,5)
 9  ┃ def foo () 
    ╋     ^^^
    ┃ Note : 
    ┃  --> main.yr:(10,12)
    ┃ 10  ┃     throws &ParentException
    ┃     ╋            ^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Scope guards

Scope guards are expressions attached to a scope (block of code), that are executed on specific cases in the scope that is guarded. There are four different scope guards, exit, failure, success and catch. This chapter does not discuss the catch scope guard, that will be discussed in the next chapter.

The syntax of exit, success and failure scope guards is the following:

guarded_scope := '{' expression ((';')? expression)* (';')? '}' guards
guards := (Guard expression)* ('catch' catching_expression)? (Guard expression)*
Guard := 'exit' | 'success' | 'failure'

Success guard

Scope guards are associate with expressions, that are executed when a specific events occurs in the scope that is guarded. In the case of success scope guard, the event that triggers the guard expression, is when no error occured (nothing was thrown in the scope).

import std::io;

def foo (i : i32)
    throws &AssertError
{
    println ("I : ", i);
    assert (i < 10, "i must be lower than 10");
} success {
    println ("Nothing was thrown !!");
}

def main () 
    throws &AssertError
{
    foo (1);
    foo (20);
}


Results:

I : 1
Nothing was thrown !!
I : 20
Unhandled exception
Exception in file "/home/emile/ymir/Runtime/midgard/core/exception.yr", at line 84, in function "core::exception::abort", of type core::exception::AssertError.
╭  Stack trace :
╞═ bt ╕ #1
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3 in function core::exception::abort (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:84
╞═ bt ╕ #4 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:8
╞═ bt ╕ #5 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #6
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #7 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #8
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #9 in function _start
╰
Aborted (core dumped)


This scope guard can be used on scope that never throw exceptions, in that case it is always executed. This goal of this scope guard is to execute operation at the end of a scope, only when the operation succeded (e.g. writting logs, sending acknolegement, etc.). It can be coupled with other scope guards, to perform different operation when the scope didn't succeded.

Failure guard

The failure scope guard does the opposite of the success scope guard, meaning that the associated expression is only executed when an exception was thrown in the scope that is guarded. This scope guard is really useful to perform operation without recovering from the error. Indeed, the failure guard is not a catch guard, and the execption that is thrown in the guarded scope continue its journey, but the expression in the scope guard are guaranteed to be executed (e.g. logging the error, closing a socket, unlocking a mutex, etc.).

import std::io;

def foo (i : i32)
    throws &AssertError
{
    println ("I : ", i);
    assert (i < 10, "i must be lower than 10");
} failure {
    println ("Well there was an error...");
}

def main () 
    throws &AssertError
{
    foo (1);
    foo (20);
}


Results:

I : 1
I : 20
Well there was an error...
Unhandled exception
Exception in file "/home/emile/ymir/Runtime/midgard/core/exception.yr", at line 84, in function "core::exception::abort", of type core::exception::AssertError.
╭  Stack trace :
╞═ bt ╕ #1
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #4 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:8
╞═ bt ╕ #5 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #6
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #7 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #8
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #9 in function _start
╰
Aborted (core dumped)

Exit guard

The exit scope guard is the combination of the success and failure guards. The operation contained in the guards are always executed, no matter what happened in the scope that is guarded. It can be seen as a shortcut for the success and failure guards doing the same operations, but avoiding code repetition.

import std::io;

def foo (i : i32)
    throws &AssertError
{
    println ("I : ", i);
    assert (i < 10, "i must be lower than 10");
} exit {
    println ("The scope is exited, with an error or not, who knows");
}

def main () 
    throws &AssertError
{
    foo (1);
    foo (20);
}


Results:

I : 1
The scope is exited, with an error or not, who knows
I : 20
The scope is exited, with an error or not, who knows
Unhandled exception
Exception in file "/home/emile/ymir/Runtime/midgard/core/exception.yr", at line 84, in function "core::exception::abort", of type core::exception::AssertError.
╭  Stack trace :
╞═ bt ╕ #1
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #4 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:8
╞═ bt ╕ #5 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #6
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #7 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:12
╞═ bt ╕ #8
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #9 in function _start
╰
Aborted (core dumped)

Scope guard priority

It is possible to use multiple scope guards for the same scope. In that case, the order of execution of the scopes is the following :

    1. for the scope guards of same nature (e.g. two failure guards), the execution is done is the order they are written.
import std::io;

def main () 
{
    {
        println ("Scope operation");
    } exit {
        println ("Exit 1"); 
    } exit {
        println ("Exit 2");
    }
}


Results:

Scope operation
Exit 1
Exit 2


    1. If there is an exit guard and a success or a failure guard, then the success and failure guards are executed first.
import std::io;

def main () 
{
    {
        println ("Scope operation");
    } success {
        println ("Success");
    } exit {
        println ("Exit 1"); 
    } exit {
        println ("Exit 2");
    } success {
        println ("Success 2");
    } 
}


Results:

Scope operation
Success
Success 2
Exit 1
Exit 2


    1. The priority between failure and success is not defined, they simply cannot happen at the same time.

Catching exceptions

The main idea of exception is the possibility to recover from a failing program state. In order to do that, another scope guard exits in Ymir, this scope guard is named catch. The syntax of this scope guard is relatively close to the other scope guard, and to the pattern matching. Indeed, this scope guard does not execute an expression but match over an exception that has been caught. The following code block present the grammar of the catch scope guard. The pattern_expression used in this code block are those defined in the chapter Pattern matching.

guarded_scope := '{' expression ((';')? expression)* (';')? '}' guards
guards := (Guard expression)* ('catch' catching_expression)? (Guard expression)*
Guard := 'exit' | 'success' | 'failure'

catching_expression := pattern_var | pattern_call | '_'

pattern_var := (Identifier | '_') ':' (type | '_') ('=' pattern_expression)?
pattern_call := (type | '_') '(' (pattern_argument (',' pattern_argument)*)? ')'
pattern_arguments := (Identifier '->')? pattern_expression

Catch everything

Catch scope guard can be used to catch any exception and continue the execution of the program. In the following example, the main function calls the foo function, that throws an &AssertError. The call is guarded by a catch expression, that catch every kind of Exception (using the _ token). Because the exception of the foo function is caught the main function is considered safe, and thus cannot throw an exception, this is why nothing is declared in its prototype. In this example, the program ends normaly, after exiting the main function.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo (10); 
    } catch {
        _ => {
            println ("Foo failed");
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed
After foo


A variable pattern can also be used to get the value of the exception. There is no much change in the following example, in comparison to the previous one, except that the main function prints the exception that has been throw by the foo function. In debug mode (-g option of the compiler), when throwing an exception, the stack trace is accessible and printed, when printing an exception. This stack trace (for efficiency reasons) is not created in release mode.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo (10); 
    } catch {
        err : _ => {
            println ("Foo failed : ", err);
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed : core::exception::AssertError (i must be lower than 10):
╭  Stack trace :
╞═ bt ╕ #1 in function core::exception::AssertError::self (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:49
╞═ bt ╕ #2 in function core::exception::abort (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:84
╞═ bt ╕ #3 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:7
╞═ bt ╕ #4 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:14
╞═ bt ╕ #5
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #6 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #7
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #8 in function _start
╰
After foo

Catch a specific exception

The content of a catch scope guard is a list of patterns, that can be used to get a different behavior for different kind of exception. In the following example, the foo function, can throw two different kind of exception, &AssertError and &OutOfArray. The catch of the main function has a different behavior if the exception that is thrown is a &AssertError or a &OutOfArray, (by using a variable pattern for &AssertError, and a call pattern for &OutOfArray).

import std::io;

def foo (i : [i32]) 
    throws &AssertError, &OutOfArray
{
    assert (i [0] < 10, "i[0] must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        OutOfArray () => {
            println ("Foo failed, the slice was empty");
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed, the slice was empty
After foo


Rethrowing exceptions

Catch scope guard must catch every exceptions that are thrown by the scope that is guarded. For example, if the main function of the previous example, was defined as presented in the next code block, the compiler would have returned an error. Indeed, in this example, the &OutOfArray exception is not managed by the catch guard.

def main () {
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
    }
    println ("After foo");
}


Errors:

Error : the exception &(core::array::OutOfArray) might be thrown but is not caught
 --> main.yr:(13,7)
13  ┃ 		foo ([]); 
    ╋ 		    ^
    ┃ Note : 
    ┃  --> main.yr:(14,4)
    ┃ 14  ┃ 	} catch {
    ┃     ╋ 	  ^^^^^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


The OutOfArray exception can be rethrown inside the catch scope guard to remove this error. In that case the stack trace is still correct, as it is created by the constructor of the Exception class.

def main () 
    throws &OutOfArray
{
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        x : &OutOfArray => throw x;
    }
    println ("After foo");
}

Catch as a value

In some cases (many cases), a scope is used to compute a value. In that circumstance, if an error occurs inside the scope, the value of the scope is not set, and cannot be used. For example, in the following source code, the main function tries to initialize a variable from the value of the foo function. This function is not safe, and might return no value at all. In order to guarantee, that the variable x is initialized, and usable no matter what happened in the foo function, the catch scope guard can be used as the default value. This is presented as well at line 21 to initialize the variable y.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i[0] must be lower than 10");
    i + 12
}

def main ()
{
    {
        let x = foo (10); 
        println (x);
    } catch {
        _ => {
            println ("Foo failed");
        }
    }

    let y = {
        foo (10)
    } catch {
        _ => {
            42
        }
    }

    println (y);
}


Results:

Foo failed
42


Because, the catch scope guard is a pattern matching, multiple branch can be entered. In that case, every branch of the catch guard must share the same type. In the following example, the value y is set conditionaly, depending on the type of exception that is thrown by the foo function, or if the function foo succeeds. In this example, the foo function throws a &OutOfArray exception, thus the variable y is set to 42 (value computed at line 19).

import std::io;

def foo (i : [i32]) -> i32
    throws &AssertError, &OutOfArray
{
    assert (i[0] < 10, "i[0] must be lower than 10");
    i[0] + 12
}

def main ()
{
    let y = {
        foo ([])
    } catch {
        AssertError () => {
            11
        }
        OutOfArray () => {
            42
        }
    }

    println (y);
}


Results:

42

Catch along other scope guards

Catch scope guards can be used along other scope guards, (success, failure and exit), but there can be only one catch scope guard per scope . In that case the priority is the following: 1) failure,

  1. catch, 3) exit. If there is a success scope guard that is executed, then the catch scope guard cannot be executed, so the priority over these two guards is not defined.
import std::io;

def foo (i : [i32]) 
    throws &AssertError, &OutOfArray
{
    assert (i [0] < 10, "i[0] must be lower than 10");
    println (i);
}

def main ()
{
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        _ : &OutOfArray => {
            println ("Out");
        }
    } success {
        println ("Succ");
    } failure {
        println ("Fails");
    } exit {
        println ("Exit");
    }
    println ("After foo");
}


Results:

Before foo
Fails
Out
Exit
After foo


The behavior is exactly the same when catch scope guard has a value.

import std::io;

def foo (i : [i32]) -> i32
    throws &AssertError, &OutOfArray
{
    assert (i[0] < 10, "i[0] must be lower than 10");
    i[0] + 12
}

def main ()
{
    let y = {
        foo ([])
    } catch {
        AssertError () => {
            println ("Assert");
            11
        }
        OutOfArray () => {
            println ("Out");
            42
        }
    } failure {
        println ("Fails");
    } exit {
        println ("Exit");
    }

    println (y);
}


Results:

Fails
Out
Exit
42

Relation between exception and option type

We have seen in the chapter Basic programming concept, that the Ymir language have a primitive option type. This type has a really close relation with exceptions. Indeed, any value can be store inside an option type, and any failing scope can be used to create an empty option value. The token ? is used to transform a value into an option type.

In the following example, the foo function can throw an exception. The main function calls it, but use enclose the result inside an option type, to handle the errors. The type of the variable x is then (i32)?, meaning option of i32. A pattern matching, is then used to check wether the x contains a value or not. In this example, a new pattern of the pattern matching is presented. This pattern is specific to the case of option type, and adds two keywords Ok, and error Err. They can be used with or without internal pattern (never named), to get the content of the option (in case of Ok), or the exception (in case of Err).

import std::io;

def foo (i : i32)-> i32 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    i + 12
}

def main () {
    let x = foo (10) ?
    match x {
        Ok (y : _) => { 
            println ("x contains the value : ", y);
        }
        Err (err : _) => {
            println ("x has no value, because : ", err);
        }
    }
}


Results (in release mode) :

x has no value, because : core::exception::AssertError (i must be lower than 10)


One can note from the previous example, that the main function is safe. The option enclosing catches every exceptions. The exception can then be retreived by using a pattern matching, as presented above. Warning Unfortunately, the accuracy of the exception type that is thrown is lost at compile time, (i.e. the type contained inside an option type is Exception). Even, if the type can be specifically retreived at execution time. To remove this limitation, the different type of exception would have to be written in the definition of the option type. This would be extremely verbose. Contribution Maybe adding the possibility to write it, but optionaly, in order to store it if possible. I don't know if it is a good idea.

Empty option without exception

It is possible to initialize an empty option value without throwing an exception. This can be done using the type specific attribute err. In that case, the option does not have a value, even if it is an Err value.

import std::io;

def foo ()-> i32? {
    (i32?)::__err__
}

def main () {
    match foo () {
        Err (err : _) => {
            println ("Empty but with exception : ", err);
        }
        Err () => {
            println ("Totally empty, init with __err__");
        }
    }
}


Results:

Totally empty, init with __err__

Void option type

Option type can store values of type void, in that case the Ok pattern has no value to get. This can be usefull to execute a function, and verify afterwards if it succeeded or not.

import std::io;

def foo (i : i32)-> void 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}

def main () {
    let x : void? = foo (10) ?
    match x {
        Ok () => { 
            println ("Foo succeeded");
        }
        Err (err : _) => {
            println ("Foo failed, because : ", err);
        }
    }
}


Results:

Foo failed, because : core::exception::AssertError (i must be lower than 10)

Function pointer and closure

It is impossible to create a function pointer or closure from a function that can throw exceptions. Indeed, because funtion pointers type definition does not include the possibility to throw exception (and will not for verbosity, and annoyance reason), the Ymir language does not allow them to throw exception, in order to keep the guarantee of safety introduced by exception rethrowing. For that reason, the following example is not accepted by the compiler.

import std::io;


def main () {
    let x = |i : i32| => {
        assert (i < 10, "i must be lower than 10");
        println (i);
    };
        
     x (10);
}


Errors:

Error : a lambda function must be safe, but there are exceptions that are not caught
 --> main.yr:(5,13)
 5  ┃     let x = |i : i32| => {
    ╋             ^
    ┃ Note : &(core::exception::AssertError)
    ┃  --> main.yr:(6,9)
    ┃  6  ┃         assert (i < 10, "i must be lower than 10");
    ┃     ╋         ^
    ┗━━━━━┻━ 

Error : undefined symbol x
 --> main.yr:(10,6)
10  ┃      x (10);
    ╋      ^


ymir1: fatal error: 
compilation terminated.


To avoid that problem, every exception must be caught inside the lambda function.

Create function pointer from unsafe function

Sometimes it can be usefull to create function pointer from functions that are not safe (for example the function foo in the following example). To do that, the core modules, define a template function (cf. Templates, named toOption that transform a function symbol, into another function symbol that returns an option value. This other function symbol can be used to create a function pointer, using the ampersand (&) unary operator.

import std::io;

def foo (i : i32)-> void
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}


def main () {
    let x = & (toOption!foo);
    println (x (10));
    println (x (3));
}


Results:

Err(core::exception::AssertError (i must be lower than 10))
3
Ok()


Contribution This is not possible for method delegate.

Templates

The templates system provide the possibility to reuse source code, that is valid for multiple types. The template system of Ymir is powerful, and allows the generation of code, that will be used many times for many purpose, by writting minimal source code, and conditional compilation. Templates is a main part of the Ymir language, and almost everything in the standard library is written using templates. It is important to understand the template system, to use the language.

Template definition syntax

Multiple symbols in Ymir can be templates. Every template symbol has a name, and the template parameters are following that name enclosed between curly brackets (this time they are always mandatory). For example, a function can be a template, as it can be seen in the following example. In this example, the function foo takes a type as template parameter, and this type is named T in the function symbol, and is used as the type of the first parameter of the function (i.e. the type of the parameter a). By convention, the identifiers of the template parameters are in upper case, however that is not mandatory.

def foo {T} (a : T) {
    println (a);
}


Other symbols can also be templates. These symbols are :

  • Classes
  • Structures
  • Enumerations
  • Local modules
  • Traits
  • Aka

The templates arguments always follows the name of the symbol. In the following example, templates are defined for various symbols.

class A {T} {
    let value : T;
    
    pub self (v : T) with value = v {}
}

struct 
| x : T
-> S {T};

enum 
| X = cast!T (12)
-> F {T};

mod Inner {T} {
    pub def foo (a : T) {
        println (a);
    }
}

trait Z {T} {
    pub def foo (self, a : T)-> T;
}

aka X {T} = cast!T (12);


Template argument syntax

The template call syntax is declared using the token !, followed by one are multiple arguments, enclosed inside curly brackets. Template arguments are elements that must be known by the compiler at compile time, in order to produce a valid template specialization and create a symbol that can be used and is fully validated (i.e. where every types are correctly defined). The following code block present the syntax of the template call. Template call is a high priority expression, that has a even higher level of priority than the :: operator, and unary operators. Operator priority is presented in the chapter Operator priority.

template_call := expression (single_arg | multiple_args) 
single_arg := '!' expression 
multiple_args := '!' '{' (expression | template_call) (',' (expression | template_call))*)? '}'


And the following code block presents example of template call on a function named foo.

foo!i32 (12); // One template argument (i32)
foo!(i32, f32) (12); // One template tuple (i32, f32)
foo!{i32, f64} (12); // Two template arguments, types i32 and f64


When the arguments, are also template, the curly brackets are mandatory even if there is only one parameter, to avoid ambiguity.

foo!{foo!i32} (); // Ok
foo!foo!i32 (); // No


Template instanciation

When a template symbol is defined, the template call is used to reference it, and make a specialization. The arguments used in the template call are associated to the template parameters of the template symbol, in the order they are defined. In the following example, a function foo has a template argument, that must be a type, and is named T. The main function use the template call syntax to use that symbol, and associate to T the type i32. The symbol with a i32 is then created by the compiler, and the main function calls it using the standard call syntax using the parentheses operator.

import std::io;

def foo {T} (a : T) {
    println (a);
}
    
def main () {
    foo!i32 (42);
}


Results:

42


When the template symbol is a function, it can happened that the template parameters can be infered from the parameters of the function. For example in the above example, there is no need to specify a template call, and the standard call expression is sufficient.

import std::io;

def foo {T} (a : T) {
    println (a);
}
    
def main () {
    foo (42); // T, is infered as i32
    foo ("Hi !!"); // T, is infered as a [c32]
}


Results:

42
Hi !!


This cannot be done for structure or module. However, this is possible to do on classes, and this will be presented a little later, in the following section of the chapter.

We have seen in the chapter about function (cf. Functions), the uniform call syntax. This syntax is also applicable on template functions. In the following example, a function that takes two types as template parameters is called in the main function.

import std::io;

def foo {T} (a : T) {
    println (a);
}

def main () {
    (42).foo (); 
}

Multiple template parameters

As said earlier the parameters are specialized using the arguments of the template call syntax in the order they are presented. For example, in the following example, the template call syntax at line 1 creates a symbol where T=i32, and U=f64.

def foo {T, U} () {}

def main () {
    foo!{i32, f64} ();
}


It is not necessary to put all the argument in the other template parameters can be infered from the previous template arguments, or by the parameters of the function. We will see in a next chapter some way to determine the kind of type that can be used in a template symbol, but briefly in the following example, the foo function only accepts types that are slices of U, where U can be any type. In that case, because T can be used to infer the type of U, there is no need to specify the type of U explicitly.

import std::io;

def foo {T of [U], U} () {
    println ("T=", T::typeid, " U=", U::typeid);
}

def main () {
    foo![i32] ();
}


Results:

T=[i32] U=i32


The same behavior can be observed when the type can be infered from a standard parameter of the function. In the following example, the type T is defined by the template call syntax, but the type U is defined by the first argument of the standard call. Thus, type T is i32, and type U is f64.

import std::io;

def foo {T, U} (a : U) {
    println ("T=", T::typeid, " U=", U::typeid, " a=", a, "");
}

def main () {
    foo!i32 (3.14);
}


Results:

T=i32 U=f64 a=3.140000


One can note that the type T cannot be infered from anything aside the template call. Thus it has to be the first template parameter, otherwise the template call would have defined the type U. In the following example, the parameter T and U have been reversed, but the call is the same. In that case, the compiler fails to create a valid symbol and throws an error.

import std::io;

def foo {U, T} (a : U) {
    println ("T=", T::typeid, " U=", U::typeid, " a=", a, "");
}

def main () {
    foo!i32 (3.14); // set U to i32, and T cannot be infered
}


Errors (in this error, we can see that U is set to i32 at line 10, and that the compiler failed to set T) :

Error : the call operator is not defined for foo {T}(a : U)-> void and {f64}
 --> main.yr:(8,10)
 8  ┃ 	foo!i32 (3.14);
    ╋ 	        ^    ^
    ┃ Note : candidate foo --> main.yr:(3,5) : foo {T}(a : U)-> void
    ┃     ┃ Error : unresolved template
    ┃     ┃  --> main.yr:(3,13)
    ┃     ┃  3  ┃ def foo {U, T} (a : U) {
    ┃     ┃     ╋             ^
    ┃     ┃ Note : for : foo --> main.yr:(3,5) with (U = i32)
    ┃     ┗━━━━━━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Using the template call syntax to set only a part of the template symbols is named a two time template validation. We will see in the next chapter, that template specialization can be very powerful and can be used to choose between multiple template symbols. Refering to a template symbol without using the template call syntax can be seen as a special case of two time validation, where the template call is made but with no arguments.

Template class instanciation

When a class template is declared, the compiler is sometimes able to infer the type of the templates from the argument passed to the constructors. The rule is the same as for function instanciation. In the following example, the class X is a template class that takes two types as template parameters. The main function instanciate a X class at line 10 without using the template call syntax. This is possible, because the constructor of the class at line 6 is sufficient to infer the types T and U exactly as it would be done if it was a function template. Because T and U has no restriction any type can be used.

import std::io;

class X {T, U} {
    let x : T, y : U;

    pub self (x : T, y : U) with x = x, y = y {}	
}

def main () {
    let a = X::new (1, 'r');
    let b = X::new ([1, 2], "foo");
    
    println (a::typeinfo.name);
    println (b::typeinfo.name);
}


Results:

main::X(i32,c32)::X
main::X([i32],[c32])::X


A two time validation can also be used to set the types of a part of the template parameters, and let the other be infered by the constructor call. In the following example, the type T is set by the template call syntax, and the type U is infered from the type of the parameter y of the constructor (here c32).

import std::io;

class X {T, U} {
    let y : U;

    pub self (y : U) with y = y {}	
}

def main () {
    let x = X!(i32)::new ('r');
    println (x::typeinfo.name);
}


Results:

main::X(i32,c32)::X

Contribution other template symbols cannot be called without template call. This is normal for modules, traits, and enumeration, as nothing can be used to infer the types. But structures are called using arguments, that are used to set the values of the fields, this is thus possible to infer the templates types in that case. Has to be done, though.

Template specialization

We have seen in the last chapter, the declaration of template symbol, that can be instanciated using any types. Sometimes, it can be usefull to restrict the set of types that can be used in a given template symbol (e.g. only slices but of any type, only classes, only structures, etc.). In order to make this possible, the Ymir language offers some elements to filter the different types that can be used.

The following code block present the complete syntax of a template parameter.

template_parameter := Identifier | of_filter | class_filter | struct_filter | impl_filter | variadic_filter

of_filter := Identifier 'of' type
class_filter := 'class' Identifier
struct_filter := 'struct' Identifier
impl_filter := Identifier impl type
variadic_filter := Identifier '...'


One can note that every template parameters have an Identifier. This identifier are the root of the specialization tree. For example, in the following template parameters {T of [U], U} there are two roots, T and U. The root U is important, these template parameters are different to {T of [U]}. In the first case, the identifier U refers to a template type (as it is a root of the parameters), and in the second case it refers directly to a type that is named U, and that has to be declared somewhere.

Of filter

The of filter is declared using the keyword of. This filter is used to specify the form of the type that can be used to instanciate the template. The form can be any form of type, it can be for example a slice, an array, a template symbol, etc. In the following example, the first function foo declared at line 6, is a template function that can be instanciated using only T=i32. The second function foo at line 14 use the of filter to inform that the type T must have the same form as the type Z, but does not filter the type Z. The third function foo at line 21, accepts for the type T any slice that have as internal type a template type declared as Z, there is no filter on Z.

import std::io;

/**
 * This function will only be callable, with a i32 as T
 */
def foo {T of i32} (_ : T) {
    println ("First ??");
}

/**
 * This function will only be callable, with a type that fit the Z pattern
 * That is to say every type
 */
def foo {T of Z, Z} (_ : T) {
    println ("Second ?");
}

/**
 * This function will be only callable, with a slice of Z, where Z can be anything
 */
def foo {T of [Z], Z} (_ : T) {
    println ("Third !");
}

def main () {
    foo ([1, 2]);
}


In the above example, one can note that, two functions can be called by the expression foo ([1, 2]). The second and third ones. The template definition that match the best the types, will be used. Here, this is the third one, the filter is more specific and thus has a better score.

Results:

Third !


The of filter is a kind of destructuring pattern. They can be chained, and composed with other filters. Here some other example, where this time the of filter is used to get the template types parameters of a given class type, and apply some filter on them.

import std::io;

/** A template class, that takes any type as template parameters */
class X {T} {
    let _x : T;
    
    pub self (x : T) with _x = x {}
}

/**
 * This function accepts any X object, as long as its template parameter is a slice
 */
def foo {T of &(X!{U}), U of [Z], Z} (x : T) {
    println ("Slice X : ", x::typeinfo.name);
}

/**
 * Accept all the X objects, that have not been accepted by the first function
 * Indeed, the template is less specific, and is used only if the first one fails
 */
def foo {T of &(X!{U}), U} (x : T) {
    println ("Not a slice X : ", x::typeinfo.name);
}


def main () {
    let a = X::new ([1, 2, 3]);
    let b = X::new ("Test");
    let c = X::new (23.0f);
    
    foo (a);
    foo (b);
    foo (c);
}


Results:

Slice X : main::X([i32])::X
Slice X : main::X([c32])::X
Not a slice X : main::X(f32)::X


As presented in the introduction of this chapter, there is a difference between identifier that can be found in the roots of the template parameters and those which are not. In the following example, the foo function accept a slice of U where U is not defined, resulting in an error by the compiler. Indeed, the type U could have been defined somewhere, and there must be a distinction between this type and a template parameter.

import std::io;

def foo {T of [U]} (a : T) {}

def main () {
    foo ([1, 2]);
}


Errors:

Error : the call operator is not defined for foo {T of [U]}(a : T)-> void and {mut [mut i32]}
 --> main.yr:(6,6)
 6  ┃ 	foo ([1, 2]);
    ╋ 	    ^      ^
    ┃ Note : candidate foo --> main.yr:(3,5) : foo {T of [U]}(a : T)-> void
    ┃     ┃ Error : undefined type U
    ┃     ┃  --> main.yr:(3,16)
    ┃     ┃  3  ┃ def foo {T of [U]} (a : T) {}
    ┃     ┃     ╋                ^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Struct and Class filters

The keywords struct and class defines respecitvely the struct and class filters. They filter the accepted types of a template symbol. Unlike of filter they cannot be chained, and accept only an identifier. In the following example, the first foo function at line 11 is instanciable using a class type, and the second at line 15 only accept struct types. Warning class filter accepts a reference class type, and not directly the class type (&A not A).

import std::io

struct 
| x : i32
-> X;

class A {
    self () {}
}

def foo {class T} () {
    println ("Class !");
}

def foo {struct T} () {
    println ("Struct !");
}

def main () {
    foo!(&A) ();
    foo!(X) ();
} 


Results:

Class !
Struct !


Even if this filters cannot be chained, they can be used as the leaf of a of filter. In the following example, the function foo accepts a slice of class objects.

import std::io;

class A {
    pub self () {}
    impl Streamable;
}

class B {
    pub self () {}
    impl Streamable;
}

def foo {T of [U], class U} (a : T) {
    println (T::typeid, " = ", a, "");
}

def main () {
    foo ([A::new (), A::new ()]);
    foo ([B::new ()]);
}


Results:

[&(main::A)] = [main::A(), main::A()]
[&(main::B)] = [main::B()]

Implement filter

We have seen in the chapter about traits (cf. Traits), that class type can implement a given trait. Implementing a trait gives specific method to a class type, that can be called. However traits lose most of their interest if it is impossible to accept a type that implements the trait without knowning the type itself. This cannot be done by inheritance, as traits are not types, however templates have a specific filter to perform this operation. In the following example, the impl filter is used by the foo function, that thus accepts any kind of object as long as they impl the trait Getter.

import std::io;

trait Getter {
    pub def get (self)-> i32;
}

class A {

    pub self () {}
    
    impl Getter {
        pub over get (self)-> i32 {
            12
        }
    }

}

def foo {T impl Getter} (a : T) -> i32 {
    a.get ()
}

def main () {
    let a = A::new ();
    println (foo (a));
}


A trait can be a template symbol. In that case it has some template parameters, that can be destructured by a template filter. For example, in the following source code, the trait Getter is a template, that is implemented using the type i32 by the class A. The first foo function at line 1 accepts any kind of object as long as they impl the trait Getter, and filter the template parameter of this trait to get it under the identifier X. The second foo function only accepts types that implements the trait but using a slice. Because the second foo function is more specific when using &B object, it is called at line 44.

import std::io;

trait Getter {T} {
    pub def get (self)-> T;
}

class A {

    pub self () {}
    
    impl Getter!{i32} {
        pub over get (self)-> i32 {
            12
        }
    }
}

class B {

    pub self () {}
    
    impl Getter!{[i32]} {
        pub over get (self)-> [i32] {
            [12, 24]
        }
    }
}

def foo {T impl Getter!{X}, X} (a : T) -> X {
    print ("First : ");
    a.get ()
}

def foo {T impl Getter!{X}, X of [U], U} (a : T)-> X {
    print ("Second : ");
    a.get ()
}

def main () {
    let a = A::new ();
    let b = B::new ();
    
    println (foo (a));
    println (foo (b));
}


Results:

First : 12
Second : [12, 24]


Variadic templates

Variadic templates are special templates, that takes an arbitrary number of type as arguments. They are defined using an identifier followed by the token .... When the specialization is done, the identifier of the variadic template, can be used to define a tuple type. In the following example, the type of the parameter a of the foo function is (i32, i32, i32, i32, i32). Warning If only one type is given to the variadic template, then it is not a tuple, but directly the type that has been given. As you may have guessed by now, the println function is a variadic template function.

import std::io

def foo {T ...} (a : T) {
    println (a.0, expand a);
}

def main () {
    foo (1, 2, 3, 4, 5);
}


Results:

112345


The identifier can also be used to complete another type. For example, a function pointer type. In the following source code, the structure X accepts an arbitrary number of type as template parameters, and use them to form the type of the field foo. When instanciated by the main function at line 12, the field foo takes the type fn (i32, f32)-> void.

import std::io

struct 
| foo : fn (T)-> void
 -> X {T...};
 
def foo (x : i32, y : f32)-> void {
    println ("(", x, ", ", y, ")");
}
 
def main () {
    let x = X!{i32, f32} (&foo);
    x.foo (12, 3.14f);
}


Results:

(12, 3.140000)


To force the type to be a tuple inside another type, the standard syntax of tuple can be used. For example, the field foo could have been defined as follows fn ((T,))-> void, in that case it would have been equal to fn ((i32, f32))-> void, meaning a function pointer that takes a parameter of type (i32, f32), and returns nothing.

Recursive variadic template

Variadic template must contain at least one type. To perfom recursive variadic function, end case functions must be written, this end case generally contains a standard template parameter. For example, the following example presents a foo function that takes variadic parameters, and prints them. The end case is described at line 3, where the function takes only one standard template parameter. The function foo at line 8 takes two template parameter, a standard one that will be used for the first parameter of the function, and a variadic one for the rest of the parameters. One can note from the line 5 that even if the type of b is c8 and thus not a tuple, the keyword expand is usable, and does nothing particular.

import std::io;

def foo {F, R...} (a : F, b : R) {
    println ("FST : ", F::typeid, "(", a, ")");
    foo (expand b);
}

def foo {F} (a : F) {
    println ("SCD : ", F::typeid, "(", a, ")");
}

def main () {
    foo (1, 3.f, "Test", 'r'c8);
}


Results:

FST : i32(1)
FST : f32(3.000000)
FST : [c32](Test)
SCD : c8(r)

Template values

In Ymir, templates are seen as compilation time execution parameters. These parameters can be either types or values. When dealing with values, as with any values, decisions and program branching can be made, but because these values are known at compilation time, the decisions can also be made at compilation time. This system is called compilation time execution or cte for short. The keyword cte is used to ensure that a part of the code is executed at compilation time, and generates a value (that can be void) at compilation as well, to save time at execution time and have a better optimized executable.

Compilation time values

Basically, every values can be known at compilation time as long as they do not implies variable, or dynamic branching. For example, the value of the foo function in the following source code can be knwon at compilation time. Indeed, it implies only constants, that can be computed by the compiler directly. The main function uses the keyword cte to force the compiler to call the function foo during the compilation. If the keyword is omitted then the function foo is called at execution time.

import std::io;

def foo () -> i32 {
    bar () + baz ()
}
    
def bar () -> i32 
    12
    
def baz () -> i32
    30
    
def main () {
    let z = cte foo ();
    println (z);
}


To verify that the compilation time execution effectively happened, the option -fdump-tree-gimple can be used. This option creates alternative files, that give information about the compilation, and can be used to see what the frontend of the Ymir compiler gave to the gcc compiler (source code close the C language). The following block of code presents a part of the content of this file. One can note that the main function does not call the foo function, but only the println function with the value 42.

main ()
{
  {
    signed int z;

    z = 42;
    _Y3std2io11printlnNi327printlnFi32Zv (z);
  }
}

Values as template parameter

We have seen in the previous chapter that templates parameters are used to accept types. They also can be used to accept values, in that case the syntax - described in the following code block - is a bit different. The syntax for template values is close to variable declaration, using the token :, or by using directly the literal that is accepted.

template_value := literal | Identifier ':' (Identifier | type) ('=' literal)?

Template literal

A literal that can be known at compilation time can be used to make a template specialization. The types that can be knwon at compilation time are the following :

  • string ([c8] or [c32])
  • char (c8 or c32)
  • integer (signed or unsigned)
  • float

Contribution: tuple, and struct are not compilation time knowable, but this seems possible if they only contains cte values, same for slice that are not strings.

In the following example, there are three different definition of the function foo. The first one at line 3 can be called using a the cte value 3, the second one at line 8 using the value 2, and so on. The main function calls the function foo using the value 5 - 2, so the first definition at line 3 is used.

import std::io;

def foo {3} () {
    println ("3");
    foo!2 ();
}

def foo {2} () {
    println ("2");
    foo!1 ();
}

def foo {1} () {
    println ("1");
    foo!0 ();
}

def foo {0} () {
    println ("Go !");
}

def main () {
    foo!{5 - 2} ();
} 


Results:

3
2
1
Go !


Literal string can also be used as template parameter. We will see in a forthcoming chapter that those are used for operator overloading (cf. Operator overloading). A simple example is presented in the following source code, where the foo function accepts the literal Hi I'm foo !, and is called by the main function using different ways.

import std::io;

def foo {"Hi I'm foo !"} () {
    println ("Yes that's true !");
}

def bar () -> [c32] {
    "I'm foo !"
}

def main () {
    foo!"Hi I'm foo !" ();
    foo!{"Hi " ~ bar ()} (); 
}


Results:

Yes that's true !
Yes that's true !

Template variable

Because it would be utterly exhausting to write every definitions of the template function with every possible literals (and even impossible when dealing with infinite types such as slice), we introduced the possibilty of writing template variables. Unlike real variables those are evaluated at compilation time, and can be defined only inside template parameters. The definition syntax of template variable is close to the definition of a standard parameter, with the difference that the type can be a template type (containing root identifiers, foundable inside the template parameters). The following example presents the definition of a function that make a countdown to 0 (the generalization of the function foo presented in the first example of the previous section). For the recursivity to stop, the definition of a final case is mandatory, here it is achieved by the function foo at line 8.

import std::io;

def foo {n : i32} () {
    println (n);
    foo!{n - 1} ();
}

def foo {0} () {
    println ("Go !");
}

def main () {
    foo!12 ();
}


Results:

12
11
10
9
8
7
6
5
4
3
2
1
Go !


Limitation: to avoid infinite loops, the compiler uses a very simple verification. It is impossible to make more that 300 recursive call. For that reason, make the following call foo!300 () is impossible and generate a compilation error :

Error : undefined template operator for foo and {300}
 --> main.yr:(13,5)
13  ┃ 	foo!300 ();
    ╋ 	   ^
    ┃ Error : undefined template operator for foo and {300}
    ┃  --> main.yr:(13,5)
    ┃ 13  ┃ 	foo!300 ();
    ┃     ╋ 	   ^
    ┃     ┃ Note : in template specialization
    ┃     ┃  --> main.yr:(13,5)
    ┃     ┃ 13  ┃ 	foo!300 ();
    ┃     ┃     ╋ 	   ^
    ┃     ┃ Note : foo --> main.yr:(3,5) -> foo
    ┃     ┃ Error : undefined template operator for foo and {299}
    ┃     ┃  --> main.yr:(5,5)
    ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃ Error : undefined template operator for foo and {299}
    ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃      : ...
    ┃     ┃     ┃     ┃ Note : there are other errors, use option -v to show them
    ┃     ┃     ┃     ┃ Error : undefined template operator for foo and {2}
    ┃     ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃     ┃ Note : in template specialization
    ┃     ┃     ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃     ┃ Note : foo --> main.yr:(3,5) -> foo
    ┃     ┃     ┃     ┃     ┃ Error : undefined template operator for foo and {1}
    ┃     ┃     ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃     ┃     ┃ Error : undefined template operator for foo and {1}
    ┃     ┃     ┃     ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃     ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃     ┃     ┃     ┃ Note : in template specialization
    ┃     ┃     ┃     ┃     ┃     ┃     ┃  --> main.yr:(5,5)
    ┃     ┃     ┃     ┃     ┃     ┃     ┃  5  ┃ 	foo!{n - 1} ();
    ┃     ┃     ┃     ┃     ┃     ┃     ┃     ╋ 	   ^
    ┃     ┃     ┃     ┃     ┃     ┃     ┃ Note : foo --> main.yr:(3,5) -> foo
    ┃     ┃     ┃     ┃     ┃     ┃     ┃ Error : limit of template recursion reached 300
    ┃     ┃     ┃     ┃     ┃     ┃     ┃  --> main.yr:(3,5)
    ┃     ┃     ┃     ┃     ┃     ┃     ┃  3  ┃ def foo {n : i32} () {
    ┃     ┃     ┃     ┃     ┃     ┃     ┃     ╋     ^^^
    ┃     ┃     ┃     ┃     ┃     ┃     ┗━━━━━┻━ 
    ┃     ┃     ┃     ┃     ┃     ┗━━━━━┻━ 
    ┃     ┃     ┃     ┃     ┗━━━━━┻━ 
    ┃     ┃     ┃     ┗━━━━━┻━ 
    ┃     ┃     ┗━━━━━┻━ 
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Contribution: add an option to the compiler to change this value of 300.

Template type for template variable

The type of a cte variable can be a template. In that case the used template identifiers must be roots of the template parameters (exactly the same behavior as the of filter). In the following example, the function foo takes a value as template parameter, the type of this value can be anything as long as it can be known at compile time.

import std::io;

def foo {N : T, T} () {
    println (T::typeid, "(", N, ")");
}

def main () {
    foo!42 ();
    foo!"Hi !" ();
}


Results:

i32(42)
[c32](Hi !)


Compilation time values, can also be used to get the size of a static array at compilation time, and make a template function that accepts arrays of any size. This evidently works only on static arrays, and not on slice, because the size of the array has to be knwon at compilation time. However, this would not be necessary when using slice, because function accepting slice as parameter already accepts slices of any size.

import std::io

def foo {ARRAY of [T; N], T, N : usize} (a : ARRAY) {
    println ("Got an array of ", T::typeid, " of size : ", N);
    println (a);
}

def main () {
    let array = [0; 10u64];
    foo (array);
}


Results:

Got an array of i32 of size : 10
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Function pointers

Lambda functions and function pointers can be compile time knwon values. This is not the case for closure, and method delegates. Unlike function pointers, template function are statically written in the generated executable, thus more efficient (even if we are talking of marginal gain). To accept a function pointer, a template variable must be defined in the template parameters. In the following example, the function foo accepts a function pointer, that takes two parameters, and return a value of the same type of the parameters. This type seems to be unknown and is not inferable from the lambda function that is passed to the function at line 13, but is infered from the execution time parameters passed to the function. At line 14, one can note that a standard function can be used as a template variable.

import std::io;


def foo {F : fn (X, X)-> X, X} (a : X, b : X) {
    println ("Foo : ", F (a, b));
}

def bar (a : i32, b : i32) -> i32 {
    a * b
}

def main () {
    foo!{|x, y| => x + y} (11, 31);
    foo!bar (6, 7);
}


Results

Foo : 42
Foo : 42


In the following example, the function pointer this time return a different type from the type of the parameters it takes. This function foo applies this function pointer to all element of the slice it takes as parameters.

import std::io;

def foo {F : fn (X)-> Y, X, Y} (a : [X]) {
    for i in a {
        println ("Foo : ", F (i));
    }
}

def main () {
    foo!{|x| => cast!i64 (x) + 12i64} ([1, 2, 3]);
}


Results:

Foo : 13
Foo : 14
Foo : 15

Template values

In the previous chapter, we saw that some values can be known at the time of compilation. These values can be used for the compiler to decide which part of the code should be compiled and which part should not, using branching operations.

Compile time condition

The keyword cte is used to inform the compiler that a value can be known at compile time, and must be evaluated during the compilation. It is not the default behavior of the compiler, as the compilation would be extremely long, if every values had to be checked. This keyword can be used on if expression to execute the condition at compile time, and evaluate compile only a part of the source code. In the following example, an if expression is used to check if the template value that is passed to the foo function was lower than 10. Because it is the case only the scope of the if expression is compiled (not the scope of the else), that is why even if the scope of the else part has no sense in term of types, the compiler does not return any error.

import std::io;

def foo {X : i32} () {
    cte if X < 10 {
        println ("X is < 10 : ", X);
    } else {
        println (X + "foo");
    }
}

def main () {
    foo!{2} ();
}


Results:

X is < 10 : 2


As normal if expression, cte if expression can be chained, however the keyword cte must be repeated before each if expression otherwise the compiler consideres that they are execution time if expression.

import std::io;

def foo {X : i32} () {
    cte if X < 10 {
        println ("X is < 10 : ", X);
    } else cte if X < 25 {
        println ("X is < 25 : ", X);
    } else {
        println ("X is > 25 : ", X);
    }
}

def main () {
    foo!{2} ();
    foo!{14} ();
    foo!{38} ();
}


X is < 10 : 2
X is < 25 : 14
X is > 25 : 38

Is expression

The is expression (that must not be confused with the is operator applicable only on pointers) is used to check template specialization, and gives a cte bool value. The syntax of the is expression is similar to the syntax of a template call, following by template parameters, as presented in the following code block. The template parameters are used to create a specialization from the template arguments.

is_expression := 'is' '!' (single_arg | multiple_args) '{' (template_parameter (',' template_parameter)*)? '}'


In the following example, the foo function accepts any kind of type as template parameter, and a cte if expression is used to apply a different behavior depending on the type of X. The first test at line 1 works if the X is a i32, the second at line 2 works if X is a slice of anything.

import std::io;

def foo {X} () {
    cte if is!{X} {T of i32} {
        println ("Is a i32");
    } else cte if is!{X} {T of [U], U} {
        println ("Is a slice");
    } else cte if is!{X} {T of [U; N], U, N : usize} {
        println ("Is a static array");
    } else {
        println ("I don't know ...");
    }
}

def main () {
    foo!i32 ();
    foo![i32] ();
    foo![i32 ; 4us] ();
    foo!f32 ();
}


Results:

Is a i32
Is a slice
Is a static array
I don't know ...


Warning An is expression is not a complete template specialization, it is not attached to any code block. Thus the variable declared inside the expression are not accessible from anywhere. It is a volontary limitation, if the variable are to be used a declaration such as a function, must be made.

Cte assert

The keyword cte can be used on an assert expression. In that case the condition of the assertion must be known at compilation time. If the value of the condition is false, then an error is thrown by the compiler, with the associated message. In the following example, the assert test wether the template class T implements the traits Hashable, and throws an explicit error message.

trait Useless {}

class X {class T} {
    cte assert (is!T {U impl Useless}, T::typeid ~ " does not implement Useless");
    
    pub self () {}	
}

class B {}

def main () {
    let _ = X!{&B}::new ();
}


Errors:

Note : 
 --> main.yr:(12,14)
12  ┃     let _ = X!{&B}::new ();
    ╋              ^
    ┃ Error : undefined template operator for X and {&(main::B)}
    ┃  --> main.yr:(12,14)
    ┃ 12  ┃     let _ = X!{&B}::new ();
    ┃     ╋              ^
    ┃     ┃ Note : in template specialization
    ┃     ┃  --> main.yr:(12,14)
    ┃     ┃ 12  ┃     let _ = X!{&B}::new ();
    ┃     ┃     ╋              ^
    ┃     ┃ Note : X --> main.yr:(3,7) -> X
    ┃     ┃ Error : assertion failed : main::B does not implement Useless
    ┃     ┃  --> main.yr:(4,9)
    ┃     ┃  4  ┃     cte assert (is!T {U impl Useless}, T::typeid ~ " does not implement Useless");
    ┃     ┃     ╋         ^^^^^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Condition on template definition

Every template symbol can have a complex condition that is executed at compilation time. This condition is executed when all the template parameter have been infered, and can be used to add further test on the template parameters that cannot be done by the syntax provided by Ymir (for example accept either a i32 or a i64). The test is defined using the if keyword followed by an expression, the value of the expression must be known at compilation time. In this expression the template parameters can be used. The if keyword always followes the keyword that is used to declare the symbol (def for function, class for classes, etc.), unlike the template parameters that always follow the identifier of the symbol.

class if (is!T {U of i32}) A {T} {
    let value : T;

    pub self if (is!U {J of T}) {U} (v : U) with value = v {}
}

struct if (is!T {U of f64})
| x : T
-> S {T};
 
enum if (is!T {U of f64})
| X = cast!T (12)
-> F {T};

mod if (is!T {U of f64}) Inner {T} {
    pub def foo (a : T) {
        println (a);
    }
}

trait if (is!T {U of f64}) Z {T} {
    pub def foo (self, a : T)-> T;
}

aka if (is!T {U of f64}) X {T} = cast!T (12);


In the following example, the function foo have a simple template specialization, but only accepts i32 or i64 types, thanks to the condition test. Because u64 is not accepted, the compiler throws an error due to line 10.

import std::io;

def if (is!{X}{T of i32} || is!{X}{T of i64}) foo {X} (x : X) {
    println (x);
}

def main () {
    foo (12);
    foo (12i64);
    foo (34u64);
}


Errors:

Error : the call operator is not defined for foo {X}(x : X)-> void and {u64}
 --> main.yr:(10,6)
10  ┃ 	foo (34u64);
    ╋ 	    ^     ^
    ┃ Note : candidate foo --> main.yr:(3,47) : foo {X}(x : X)-> void
    ┃     ┃ Error : the test of the template failed with {X -> u64} specialization
    ┃     ┃  --> main.yr:(3,26)
    ┃     ┃  3  ┃ def if (is!{X}{T of i32} || is!{X}{T of i64}) foo {X} (x : X) {
    ┃     ┃     ╋                          ^^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


Template symbol with condition have a the same score than template with the same template specialization but without a condition. For that reason, in the following example, the call of foo at line 12 create an error by the compiler. To avoid this error, the reverse test must be added to the function foo defined at line 7.

import std::io;

def if (is!{X}{T of i32} || is!{X}{T of i64}) foo {X} (x : X) {
    println ("First : ", x);
}

def foo {X} (x : X) {
    println ("Second : ", x);
}

def main () {
    foo (12);
}


Errors:

Error : {foo {X}(x : X)-> void, foo {X}(x : X)-> void} x 2 called with {i32} work with both
 --> main.yr:(12,6)
12  ┃ 	foo (12);
    ╋ 	    ^
    ┃ Note : candidate foo --> main.yr:(3,47) : main::foo(i32)::foo (x : i32)-> void
    ┃ Note : candidate foo --> main.yr:(7,5) : main::foo(i32)::foo (x : i32)-> void
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.

Common tests

The module std::traits of the standard library defines some cte function that can be used to add more complex test of the type in template condition.

Function Result
isFloating true for f32 and f64
isIntegral true for any integral types (signed and unsigned)
isSigned true for any integral types that are signed
isUnsigned true for any integral types that are unsigned
isChar true for c8 and c32
isTuple true for any tuple type
import std::io, std::traits;

def if (isIntegral!{T} ()) foo {T} () {
    println ("Accept any integral type");
}

def if (isFloating!{T} ()) foo {T} () {
    println ("Accept any floating type");
}

def main () {
    foo!i32 ();
    foo!u64 ();
    foo!f32 ();
}


Results:

Accept any integral type
Accept any integral type
Accept any floating type

Common traits

Ymir defines some common traits, that are either in the core files (automatically imported modules), or in the std. This chapters presents some of the traits that are important.

Streamable

Streamable objects are objects that can be put inside a StringStream. These objects are also printable, using the standard print or println functions. Streamable trait has a default behavior, that consist in writting the typeid of the class, followed by every fields (private and protected included) of the class inside the stream.

Print objects to stdout

In the following example, two classes implements the traits Streamable, and are printed to stdout. The first class Foo does not redefine the behavior of the trait, on the other hand Bar does.

import std::io; // the trait is accessible from std::io, or std::stream

class Foo {
    
    let _i = 12;
    let _j = "Foo";

    pub self () {}
    
    impl Streamable;
}

class Bar {
    pub self () {}
    
    impl Streamable {
        pub over toStream (self, dmut stream : &StringStream)-> void {
            stream:.write ("{I am a Bar}"s8);
        }
    }	
}

def main () {
    let a = Foo::new ();
    let b = Bar::new ();
    
    println (a);
    println (b);
}


Results:

main::Foo(12, Foo)
{I am a Bar}

Write objects to StringStream

The Streamable trait is originaly used to transform an object into a [c8], inside a &StringStream. StringStream is a class provided by the module std::stream that transform types into a growing string, in a efficient manner, to avoid unefficient string concatenation. In the following example, instead of using the print function, provided by std::io, the objects are added to a StringStream, that is then printed to stdout.

import std::io; // io publically import std::stream
                

class Foo {
    
    let _i = 12;
    let _j = "Foo";

    pub self () {}
    
    impl Streamable;
}

class Bar {
    pub self () {}
    
    impl Streamable {
        pub over toStream (self, dmut stream : &StringStream)-> void {
            stream:.write ("{I am a Bar}"s8);
        }
    }	
}

def main () {
    let a = Foo::new ();
    let b = Bar::new ();

    let dmut stream = StringStream::new ();

    a.toStream (alias stream); 
    stream:.write ("\n"s8)	 // write returns the stream
          :.writeln (b) // the method write of a stringstream call the method toStream
          :.write ("Hello : ", 42); // everything can be added inside a stringstream
    
    println (stream []); // the operator [], gets the slice [c8] contained inside the stream (without copying it).
}


Results:

main::Foo(12, Foo)
{I am a Bar}
Hello : 42

Copiable

Copiable trait is a core trait, (defined in a core file, thus does not need to be imported). This trait is used to override the dcopy operator on a class. Contribution: for the moment there is no default behavior for the Copiable trait, even if it is completely possible. Copiable trait defines a method deepCopy that takes a immutable object instance, and return a deeply mutable one.

import std::io;

class Foo {
    let mut _type : [c8];
    
    pub self () with _type = "I am an original"s8 {}
    
    pub def change (mut self) {
        self._type = "I am modified"s8;
    }
    
    impl Copiable;	
    impl Streamable; // convinient for debugging
}


def main () {
    let dmut a = Foo::new ();
    let dmut b = dcopy a; // same as alias (a.deepCopy ())
    
    b:.change ();
    
    println (a);
    println (b);
}


Results:

main::Foo(I am an original)
main::Foo(I am modified)

Disposable

Disposable trait is a trait used to perform an operation at the end of the life of an object instance. Unlike class destructor, the dispose operation must be called by hand. There is no default behavior for the Disposable trait. This trait can be coupled with the with scope guard. Briefly in a word, this scope guards define a variable (or a list of variable), like the let statement, but ensure that it is disposed when the scope is exited, no matter what happend in the scope. The Disposable trait is commonly used for unmanaged memory (File, TcpStream, Mutex, ...).

import std::io;

class Foo {
    pub self () {}
    
    impl Disposable {
        
        pub over dispose (mut self) {
            println ("I am disposed");
        }			
    }	
    
    impl Streamable;
}

def main () {
    with dmut a = Foo::new () {
        println (a);
    }
    println ("After a");
}


Results :

main::Foo()
I am disposed
After a


A good practice is to call the dispose method inside the destructor of the class. This way, even if the class was not disposed by hand, it is disposed when the garbage collector destroy the instance. (Warning to avoid multiple disposing the method dispose shoud verify that the object is not already disposed, e.g. the method can be called twice by hand, or first by hand, and then by the destructor).

Hashable

Hashable trait (importable from std::hash) is used to transform an object instance into a u64. The interest is to easily compare objects, for example in std::collection::map::HashMap, or std::collection::set::HashSet. Hashable classes can be used in these collections as key. A default behavior is defined in this trait, but the method hash can be redefined, it takes a immutable object instance as parameter, and return a u64 value.

import std::io;
import std::collection::map;
import std::hash;

class Foo {
    let _v : [c8];

    pub self (v : [c8]) with _v = v {}
    
    impl Hashable, Streamable;
}


def main () {
    let dmut coll = HashMap!{&Foo, i32}::new ();
    coll:.insert (Foo::new ("X"s8), 12);
    coll:.insert (Foo::new ("Y"s8), 34);
    
    println (coll);	
    println (Foo::new ("X"s8) in coll);
}


Results:

{main::Foo(X)=>12, main::Foo(Y)=>34}
true

Packable

Packable trait (importable from std::net::packet) defines two methods pack and unpack. There is a default behavior for those two methods, and it is recommended to not override them, unless you know exactly what you are doing. The pack method takes a immutable object instance as parameter and creates a packet of [u8], encoding the object. This packet can then be sent throw network, and unpack from another processes.

import std::io;
import std::net::packet;

class Foo {
    let _v : [c8];
    
    pub self (v : [c8]) with _v = v {}
    
    impl Packable;
}


def main () {
    let a = Foo::new ("Test"s8);

    let packet = a.pack ();
    println (packet);
}


Results :

[9, 0, 0, 0, 0, 0, 0, 0, 34, 6d, 61, 69, 6e, 33, 46, 6f, 6f, 4, 0, 0, 0, 0, 0, 0, 0, 54, 65, 73, 74]


The packet can then be unpacked with the function unpack. This function returns an Object, that can be transformed in the appropriate type using pattern matching.

    {
        match unpack (packet) {
            x : &Foo  => println (x);
            _ => {
                println ("Unkown packet");
            }
        }
    } catch {
        UnpackError () => {
            println ("The packet contains unknwon information");
        }
    }


Results:

main::Foo(Test)


Warning to work properly, the unpacker must have access to the type information of the object that is packed. Otherwise, an UnpackError is thrown. To make the symbol available in the unpacking program, the class must be compiled and linked in it. For example, if a module foo contains a class Foo, and the unpacking is made inside the **main module. The command line of the compilation must be :

gyc main.yr foo.yr


It is possible to verify that the symbol are present in the executable, by running the following command (example for the class Foo of the previous example).

$ objdump -t a.out | grep Foo
0000000000410585 g     F .text	00000000000004a0              _Y3std3net6packet8Packable27__stdnetwork__unpackContentFxP9x3foo3FooSu8Zusize
0000000000447300  w    O .data	0000000000000030              _Y3foo3FooTI
0000000000410a25 g     F .text	000000000000006e              _Y3std3net6packet8Packable25__stdnetwork__packContentFP83foo3FooxP32x3std10collection3vec6VecNu83VecZv
000000000040eb82 g     F .text	0000000000000054              _Y3foo3Foo4selfFxP9x3foo3FooSc8ZxP9x3foo3Foo
00000000004472f0  w    O .data	0000000000000010              _Y133foo3Foo_nameCSTxSc32
0000000000447340  w    O .data	0000000000000030              _Y3foo3FooVT
000000000040f468 g     F .text	0000000000000093              _Y3std3net6packet8Packable4packFP83foo3FooxP32x3std10collection3vec6VecNu83VecZv
000000000040f381 g     F .text	00000000000000e7              _Y3std3net6packet8Packable4packFP83foo3FooZSu8
00000000004472c0  w    O .data	0000000000000024              _Y183foo3Foo_nameInnerCSTxA9c32


In the result of this command, the symbol _Y3foo3FooTI is present, this is the symbol containing the type info of the foo::Foo class, _Y3foo3FooVT contains the vtable, and _Y3std3net6packet8Packable27__stdnetwork__unpackContentFxP9x3foo3FooSu8Zusize the function called to unpack the object. These are the three mandatory symbols to successfully unpack a object (the other symbols are necessarily there if the vtable is present).

I think this is not a strong problem, as it can be easily resolved; but must be taken into account when compiling the program.

Contribution: Almost every types are packable. I think the only two types that are not are function pointer and closure. I see how function pointer packing can be done, and it is not difficult and will be done in future version of the std. On the other hand, because the type info of the closure is not accessible, i don't see any way of packing this type. In any case, closure behavior can be easily simulated by using a object instance, or a structure and a function pointer. And trying to pack a not packable type creates compile time errors, so there is no bad surprise at runtime.

Serializable

Like Packable trait, Serializable is a trait that transform an object instance into something that can be stored, or sent. However, unlike Packable, the result is humanely readable, and can be used to create configuration files for example (current std implements toml and json serialization). Serializable objects implements the method serialize. It has no default behavior (Contribution it is however probably possible to create a default behavior based on the name of the fields). This method takes an immutable object instance, and return a &Config value.

import std::io;
import std::config::conv;
import std::config, std::config::toml;


struct
| A : &Foo
| B : &Foo
 -> Bar;

class Foo {
    let _v : [c32];
    let _u : i32;
    
    pub self (v : [c32], u : i32) with _v = v, _u = u{
    }

    impl Serializable {
        pub over serialize (self)->  &Config {
            let dmut d = Dict::new ();
            d:.insert ("v", self._v.to!(&Config) ());
            d:.insert ("u", self._u.to!(&Config) ());

            d
        }
    }
}


def main () {
    let x = Bar (Foo::new ("Test", 12), Foo::new ("Test2", 34));	
    println (toml::dump (x.to!(&Config) ()));
}


Results :


[A]
u = 12
v = "Test"

[B]
u = 34
v = "Test2"

Operator overloading

Ymir proposes the possibility of overloading the operators. The operator overload is done by rewriting the operations applied on objects operands. No new syntax is used to define operator overloading, as compilation time values (cf. ) are used.

There are multiple types of operators that can be overloaded, the unary operators, binary operators, comparison operators, access operator, contain operator, and the for loop. The presentation is divided in two parts, the first one presents the operator that are generally applicable to any type, and the second part presents the operator overloading of set objects.

Simple operator overloading

This chapter presents the standard operator overloading : unary, binary and comparison.

Unary operator

Unary operators are operators that are applied to only one operand. The overloading of the operator is made by defining a template method inside the class definition. The name of the template method must be opUnary, and must take a template value as first argument. The table bellow lists the rewrite operations that are done by the compiler to call the correct template method for operator overloading.

expression rewrite
-e e.opUnary!("-")
*e e.opUnary!("*")
!e e.opUnary!("!")

In the following example, the class A has two opUnary methods. The first one at line 8, is applicable with the operator -, and the second one at line 12 is applicable with any other operators.

import std::io;

class A  {
    let _a : f32;
    
    pub self (a : f32) with _a = a {} 
    
    pub def opUnary {"-"} (self) -> &A {
        A::new (-self._a)
    }
    
    pub def opUnary {op : [c32]} (self) -> &A {
        cte if (op == "!") // op is compile time known
            A::new (1.f / self._a)
        else // operator '+'
            self
    }

    impl Streamable;
}

def main () {
    let a = A::new (10.0f);
    println (!a); 
}


This example, call the method defined at line 12 by using the operator !. In this method the value of op is known at compile time, and thus can be compared (also at compile time). The ! unary operator is defined for the class A as giving the inverse of the value stored in the object, thus the result is the following :

main::A(0.10000)

Binary operator

Binary operators are also overloadable. As for unary operators, the overloading of binary operators is made by code rewritting at compile time. In the case of binary operators, the operation involves two different operands, one of them must be an object instance.

The following operators are overloadable. The use indicated in the left column is only an indication and corresponds to the common use of these operators, but they can of course be used for other purposes.

Math + - * / % ^^
Bitwise | & ^ << >>
Array ~

The following example presents a class A that overload the operator + and - using a i32 as a second operand.

import std::io;

class A {
    let _a : i32;

    pub self (a : i32) with _a = a {}

    pub def opBinary {"+"} (self, a : i32) -> &A {
        A::new (self._a + a)
    }

    pub def opBinary {"-"} (self, a : i32) -> &A {
        A::new (self._a - a)
    }
    
    impl Streamable;
}

def main () {
    let a = A::new (12);
    println (a - 30);
}


Results:

main::A(-18)


Because there are two operands (sometimes of different types), binary operation can sometimes be not commutative (for example the math binary operator -). To resolve that problem the rewritting is made in two different steps, the first step tries to rewritte the operation using the method opBinary, if this first rewritte failed a second rewritte is made, but this time using the right operand and by calling the method opBinaryRight. If the right operator is not defined, the compiler does not try to make the operation commutative, the two methods must be defined.

import std::io;

class A {
    let _a : i32;

    pub self (a : i32) with _a = a {}

    pub def opBinaryRight {"-"} (self, a : i32) -> &A {
        A::new (a - self._a)
    }
    
    impl Streamable;
}

def main () {
    let a = A::new (12);
    println (54 - a);
}


Results:

main::A(42)

Limitations

For the moment, templates method cannot be overriden by children classes. For that reason, it is impossible to override the behavior of the binary operator of an ancestor class. The limitation is the same for unary operators. However, to allow such behavior, the overloading method can call a standard method (without template), that is overridable by a children class. An example is presented in the following source code.

import std::io

class A {

    let _i : i32;
    pub self (i : i32) with _i = i {}
    
    pub def opBinary {"+"} (self, i : i32) -> &A {
        self.add (i)
    }

    pub def add (self, i : i32)-> &A {
        A::new (i + self._i)
    }

    impl Streamable;
}

class B over A {
    pub self (i : i32) with super (i) {}

    pub over add (self, i : i32)-> &A {
        B::new (i * self._i)
    }

    impl Streamable;
}

def main () {
    let mi = B::new (8);
    println (mi + 8);
}

Contribution How to override template method is currently under discussion ! But it seems impossible for many reasons that are not discussed here, you can contact us for more information.

Comparison operators

The equality and comparison are treated via two different methods opEquals and opCmp. Because while almost all types can be compared for equality, only some have meaningful order comparison.

The opCmp method is used for the operators <, >, <= and >= only. And the method opEquals is used for the operator == and !=. When the method opEquals is not defined for the type, the compiler will try to use the method opCmp instead. When both the methods are defined, it is up to the user to ensure that these two functions are consistent.

Indeed, it is impossible to verify that in a general case. For example the operator < can be used on a set object, where the operator x < y stand for x is a strict subset of y. Therefore, even if neither x < y and y < x are true, the equality x == y is not implied.

Overloading == and !=

The method opEquals is a simple method, that does not take template arguments. Expressions of the form a != b, are rewritten into !(a == b), therefore there is only one method to define.

import std::io

class Point {

    let x : i32, y : i32;
    
    pub self (x : i32, y : i32) with x = x, y = y {}
    
    pub def opEquals (self, other : &Point) -> bool {
        self.x == other.x && self.y == other.y
    }
    
}

def main () 
    throws &AssertError 
{
    let a = Point::new (1, 2);
    let b = Point::new (2, 3);
    let c = Point::new (1, 2);
    assert (a == c);
    assert (a != b);
}


The operator opEquals is assumed commutative, thus when the operator is only defined for the type on the right operand, the operation will simply be rewritten reversely. The following example presents such a case, where the operator opEquals is only defined by the class A. The line 19 is simple rewritten into mi.opEquals (8).

import std::io

class MyInt {

    let i : i32;
    
    pub self (i : i32) with i = i {}
    
    pub def opEquals (self, i : i32) -> bool {
    self.i == i
    }
    
}

def main ()
    throws &AssertError
{
    let mi = MyInt::new (8);
    assert (8 == mi);
}

Overloading <, >, <= and >=

The method opCmp is used to compare an object to another value. The comparison unlike equality evaluation, gives a comparison order between two values. The method opCmp does not take any template parameter, but returns an integer value. A negative value meaning that the left operand is lower than the right operand, an positive value, that the left operand is higher than the right one, and a nul value that both operands are equals.

The following table lists the possible rewritting of the comparison operators. As we can see in this table, the operator is assumed to be not commutative, thus if the first rewritting fails to compile (for type reason), then the second rewritting is used.

comparison rewrite 1 rewrite 2
a < b a.opCmp (b) < 0 b.opCmp (a) > 0
a > b a.opCmp (b) > 0 b.opCmp (a) < 0
a <= b a.opCmp (b) <= 0 b.opCmp (a) >= 0
a >= b a.opCmp (b) >= 0 b.opCmp (a) >= 0

In the following example, a comparison operator is used at line 19, the rewritting (7).opCmp (mi) < 0 does not compile, because 7 is not an object value, and thus does not have any method. The second rewritting is thus used, mi.opCmp (7) > 0.

import std::io

class MyInt {

    let i : i32;
    
    pub self (i : i32) with i = i {}
    
    pub def opCmp (self, i : i32) -> i32 {
        self.i - i
    }
    
}

def main ()
    throws &AssertError
{
    let mi = MyInt::new (8);
    assert (7 < mi);
}

Assignment

The assignment operator is not overloadable, it will always perform the same operation. However, the shortcut operators +=, -=, *= etc, are usable on object when oveloading the binary operator. This operation is simply rewritten at compilation time, for example the expression a += 12 is rewritten into a = a.opBinary!{"+"}(12). The following example presents an utilisation example of the += shortcut.

import std::io

class MyInt {

    let _i : i32;
    
    pub self (i : i32) with _i = i {}

    pub def opBinary {"+"} (self, a : i32)-> &MyInt {
        MyInt::new (self._i + a)
    }
    
    impl Streamable;
}

def main () {
    let mut mi = MyInt::new (8);
    mi += 9;
    println (mi);
}


Results:

main::MyInt(17)


One can note that the instance of the object stored in the variable mi is changed after the affectation. This is the standard behavior of the = operator.

Set operators

In this chapter are presented the operators related to set object. The operators are : access, contains and iteration operators.

Access operator

The operator of index [] is overloadable by the method opIndex. This method is called with the parameters passed inside of the brackets of the index operator. For example, the following operation a [b, c, d] is rewritten into a.opIndex (b, c, d).

import std::io

class A {
    let dmut i : [i32];
    
    pub self (a : [i32]) with i = copy a {}
    
    pub def opIndex (self, x : i32) -> i32 
        throws &OutOfArray
    {
        self.i [x]
    }

    impl Streamable;
    
}

def main () 
    throws &OutOfArray, &AssertError
{
    let i = A::new ([1, 2, 3]);
    assert (i [2] == 3);
}


One can note from the above example, that the object stored in i is immutable, and that the opIndex is always a right operand. It is possible to modify the values inside the i object (if it is mutable) using the opIndexAssign method. This method rewritte the assignement operation where the left operand is an access operation. The following example presents an example of usage of this method.

import std::io

class A {
    let dmut i : [i32];
    
    pub self (a : [i32]) with i = copy a {}
    
    pub def opIndex (self, x : i32) -> i32 
        throws &OutOfArray
    {
        self.i [x]
    }

    pub def opIndexAssign (mut self, x : i32, z : i32)
        throws &OutOfArray
    {
        self.i [x] = z;
    }

    impl Streamable;
    
}

def main () 
    throws &OutOfArray, &AssertError
{
    let dmut i = A::new ([1, 2, 3]);

    (alias i) [2] = 9; // alias is important, otherwise the method is not callable
    println (i);
    
    assert (i [2] == 9);
}


Results:

main::A([1, 2, 9])

Contains operator

The contain operator is a binary operator used to check if an element is inside another one. This operator is defined using the keyword in. Unlike other operator, the rewritte for the overloading is made only once on the right operand, and call the method opContains, for example the expression a in b is rewritten into b.opContains (a). The expression a !in b will be rewritten into !(a in b).

class A {
    
    let _a : [i32];
    
    pub self (a : [i32]) with _a = a {}
    
    pub def opContains (self, i : i32)-> bool {
    for j in self._a {
        if (i == j) return true;
    }
    false
    }	
}

def main () 
    throws &AssertError
{
    let i = A::new ([1, 2, 3]);
    assert (2 in i);
    assert (9 !in i);
}

Iteration operator

Object can be iterated using a for loop. As for any operators, the for loop used on object is rewritten at compilation time. There are multiple methods to write when writting an iterable class. The following source code present an example of a for loop, and the source code underneath it present its rewritten equivalent.

let a = A::new ();
for i, j in a {
    println (i, " ", j);
}


let a = A::new ();
{
    let dmut iter = a.begin ();
    while !iter.opEquals (a.end ()) {
        let i = iter.get!{0} ();
        let j = iter.get!{1} ();
        {
            println (i, " ", j);
        }
        iter:.next ();
    }
}


In this example, two elements can be highlighted: 1) the iter variable, that stores an iterator object, 2) the begin and end method of the class A. Indeed, an iterable object is an object that contains two methods begin and end, that returns an mutable iterator pointing respectivelly to the beginning and to the end of the iterable set.

The iterator type is a type defined by the user, and that contains the opEquals method, a method next on a mutable instance, and the method get, template method thats returns value pointed by the current iteration.

The following example presents the implementation of a Range that has the same behavior as a r!i32 with a step 1 and a not including the end value.

import std::io;

class Range {
    let _fst : i32;
    let _lst : i32;
    
    pub self (fst : i32, lst : i32) with _fst = fst, _lst = lst {}
    
    pub def begin (self)-> dmut &Iterator { // must return a dmut value
        Iterator::new (self._fst)
    }
    
    pub def end (self)-> &Iterator {
        Iterator::new (self._lst)
    }
    
    impl Streamable;
}

class Iterator {
    let mut _curr : i32;
    
    pub self (curr : i32) with _curr = curr {}
    
    pub def get {0} (self) -> i32 {
        self._curr
    }
    
    pub def opEquals (self, o : &Iterator) -> bool {
        self._curr == o._curr
    }

    pub def next (mut self) {
        self._curr += 1;
    }
}


def main () {
    let mut r = Range::new (0, 10);
    for i in r {
        println (i);
    }
}


To be more efficient and avoid a new allocation at each iteration, the end method should return a value that is computed once.

Contribution Enhance this section, which is completely unclear. And add information about error handling maybe.

Version

Version is another conditional compilation process (in addition to compile time execution with templates), that select parts of the code that must be compiled or not. The following code block presents the grammar of the version declaration and expression.

version_decl := '__version' Identifier '{' declaration '}' ('else' declaration)?
version_expr := '__version' Identifier block ('else' expression)?


The identifier used in the version block are in their own name space, meaning that they do not conflict with the other identifiers (variable, types, etc...). Warning the identifier of the version is not case sensitive, thus Demo and DEMO are identical. The version are activated by the command line using the option -fversion. In the following example the version Demo, and Full are used.

import std::io;

__version Demo {
    def foo () {
        println ("Foo of the demo version");
    }
} else {
    __version Full {
        def foo () {
            println ("Foo of the full version");
        }
    }	
}

def main () {
    foo ();
}


$ gyc main.yr -fversion=Demo
$ ./a.out
Foo of the demo version

$ gyc main.yr -fversion=Full
$ ./a.out
Foo of the full version


To use multiple version, the option must be set for each version.

$ gyc main.yr -fversion=Demo -fversion=Full

Debug version

The debug option of the command line -g activates the Debug version even without the option -fversion.

import std::io;

def foo () {
    __version DEBUG {
        println ("Entering foo");
    }
    
    println ("foo");
}

def main () {
    foo ();
}


$ gyc main.yr -g
$ ./a.out
Entering foo
foo

$ gyc main.yr
$ ./a.out
foo

Predefined versions

Contribution There is no predefined version for the moment, but it is a work in progress. These versions will depend on the compiler, os, etc..

Macro

Macros are used to perform operation at a syntactic level, instead of a semantic level, as it is done by all the other symbols. A macro call is an expansion of a syntaxic element.

Macros are defined using the keyword macro. They contains two kind of elements, constructors and rules.

import std::io;

macro Vec {
    pub self (x = __expr "," y = __expr z=foo) skips (" ") {
        #{z};
        println ("#{x}");
        println ("#{y}");
    }

    pub def foo (z=__expr rest=(__expr y="machin")) {
        println ("#{z}#{rest::y}");
    }
}


def main () {
    Vec#{1,2 9 9 machin};
}

Documentation

The Ymir compiler is able to generate documentation files automatically. These documentation files in json format are easier to read for a documenation generator than a source code. The option -fdoc generates documentation file for each compiled modules. The name of the json file is the path of the module, where the double colon operators :: are replace with underscores _.

$ tree
.
└── foo
    └── bar
        └── baz.yr

2 directories, 1 file

$ gyc foo/bar/baz.yr -fdoc
$ ls 
foo  foo__bar__baz.doc.json


The following chapters present the json format of the different declarations (functions, class, etc...). The first chapter presents the encoding of the types, and the second the encoding of the symbols.

Contribution A very basic standard documentation website generator is under development ydoc.

Ymir Type

Ymir type are the value type (cf. Data types). They are not always validated for many reasons (templates, aka, ...). The type value contains the kind of type that is encoded, unknown means that the type cannot be validated, and is always associated with name that contains the string name of the type.

Each type contains the attribute mut set to true or false.

Integer

Name Value
type int
name i8 V u8 V i32 etc...

Void

Name Value
type void
name void

Boolean

Name Value
type bool
name bool

Floating

Name Value
type float
name f32 V f64

Char

Name Value
type char
name c8 V c32

Array

Name Value
type array
size The size in a string
childs An array containing the inner type of the array

Slice

Name Value
type slice
childs An array containing the inner type of the slice

Tuple

Name Value
type tuple
childs An array containing the list of inner type of the tuple

Struct

Name Value
type struct
name The name of the structure

Enum

Name Value
type enum
name The name of the enum

Pointer

Name Value
type pointer
childs An array containing the inner type of the pointer

ClassPointer

Name Value
type class_pointer
childs An array containing the inner type of the pointer

Range

Name Value
type range
childs An array containing the inner type of the range

Function pointer

Name Value
type fn_pointer
childs An array containing the parameter types of the function pointer
ret_type The return type of the function pointer

Closure

Warning the closure here is the element contained inside a delegate, not the delegate type

Name Value
type closure
childs An array containing the inner types of the closure

Delegate

Name Value
type dg_pointer
childs An array containing the parameter types of the delegate pointer
ret_type The return type of the function pointer

Option

Name Value
type option
childs An array containing the inner type of the option

Unknown

Name Value
type unknown
name The string name of the type

Symbols

Each element contains standard information :

Name Value
type The type of the symbol (module, function, etc..)
name The name of the symbol
loc_file The name of the file containing the symbol
loc_line The number of the line at which the symbol is declared
loc_column The number of the column at which the symbol is declared
doc The documentation associated with the symbol (user comments)
protection The protection of the symbol (pub V prot V prv)

Module

Name Value
type module
childs The symbols declared inside the module

Function

Name Value
type function
attributes The custom attributes of the function
params The list of parameters of the function
ret_type The return type of the function
throwers The list of type that can be thrown by the function

The parameters are defined according to the following table :

Name Value
name The name of the parameter
type The type of the parameter (ymir type)
mut true V false
ref true V false
value Can be unset if the variable has no value, encoded in a string

Variable declaration

Declaration of a static global variable.

Name Value
type var
mut true V false
var_type The ymir type of the variable
value Can be unset if the variable has no value, encoded in a string

Aka

Name Value
type aka
value The value of the aka, encoded in a string

The value of the aka is encoded in a string, because as aka are only evaluated when used, we can't have more information on them.

Structure

Name Value
type struct
attributes union, packed
fields The list of fields of the structure

Fields

Name Value
name The name of the field
type The ymir type of the field
mut true V false
doc The user documentation about the field
value Set if the field has a default value, encoded in a string

Enumeration

Name Value
type enum
en_type The ymir type of the enumeration fields
fields The fields of the enum

Fields

Name Value
name The name of the field
doc The user comments about the field
value The value associated with the field, in a string

Class

Name Value
type class
ancestor Set if the class has an ancestor, ymir type
abstract true V false
final true V false
fields The fields of the class
asserts The list of static assertion inside the class
cstrs The list of constructor of the class
impls The list of implementation of the class
methods The list of methods of the class

Fields

Name Value
name The name of the field
type The ymir type of the field
mut true V false
doc The user comments about the field
protection prv V prot V pub
value Set if the field has a default value, inside a string

Asserts

Name Value
test The condition in a string
msg The msg of the assert
doc The user comment about the assertion

Constructors

Name Value
type cstr
params The list of parameters of the constructor, identical to those of a function
throws The list of ymir types thrown by the constructor

Implementations

Name Value
type impl
trait The name of the trait, in a string
childs The list of overriden methods, identical to methods

Methods

Name Value
type method
over true V false
params The list of parameters of the method, identical to function
ret_type The ymir type of the return type
attributes virtual, final, mut
throwers The list of types thrown by the method

A virtual method is method with no body, and a mutable method is a method that accepts only a mutable object.

Traits

Name Value
type trait
childs The list of method inside the trait

Templates

Name Value
type template
test Set if the template has a test, in a string
params The list of parameter of the template, in strings
childs The list of symbol inside the template

Macros

Name Value
type macro
cstrs The list of constructor of the macro
rules The list of rules of the macro

Constructors and Rules

Name Value
rule The rule of the macro in a string
skips The list of token skiped, list of string