1. 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.
1.1. 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
1.2. 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.
1.3. 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.
1.4. 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.
1.5. 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