1. 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.
1.1. 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);
}
}
1.2. 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)?
1.2.1. 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 !
1.2.2. 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
.
1.2.3. 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]
1.3. 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