Post Expressions

Grammar§

PostExpression ::= CallExpression | CastExpression | DotExpression | IndexExpression | PostDecrementExpression | PostIncrementExpression | SliceExpression | PrimaryExpression ;

CallExpression§

Grammar§

CallExpression ::= PostExpression "(" Argument? ( "," Argument)* ")" Argument ::= Expression | NamedExpression

Semantics§

Callee§

The first expression must resolve to a callable symbol. It can be either a FunctionDeclaration, a function pointer or an OverloadDeclaration The expression takes the type of its return.

Unless the callee return type is prefixed with var (see TypeReturn) a call yields a rvalue.

function get1(): s32;
if get1() do {} // rvalue
function get2(): var s32;
if get2() do {} // lvalue

However if the return type is a class, a struct, a union, a static array or a dynamic array then value returned is copied to a temporary, allowing to use the call as a lvalue.

function get(): s32[4];
assert(get().ptr);
// works because rewritten
var auto __tmp1 = get();
assert(__tmp1.ptr);

Arguments§

The expressions between parens are the call arguments. If a parameter is var, its argument must match exactly to the Type used in the declaration and must be a lvalue, otherwise the argument just have to be implictly convertible.

Named arguments§

Naming is done using the NamedExpression. A parameter can be named only once. Either all or no argument must be named.

function xyz(f32 x; f32 y; s32 z){}
0.xyz(@y 1, @x 2);  // error
xyz(0, @y 1, @x 2); // error

The usual semantics of a call are affected in the following way

  1. arguments can be passed in an arbitrar order
  2. UFC works on all the parameters, not only the first
  3. overloads are not resolved
function xyz(f32 x; f32 y; s32 z){}
(@z 0).xyz(@y 1, @x 2); // becomes              xyz(@z 0, @y 1, @x 2);
                        // then positionally    xyz(2, 1, 0);

Optional parameters are still allowed

function xyz(f32 x; f32 y; s32 z = 3){}
xyz(@y 2, @x 1); // becomes             xyz(@y 2, @x 1);
                 // then positionally   xyz(1, 2, 3);

null this checks§

If the runtime check “thiscalls” is enable and if the callee is a member function then the this argument is verified before the call and the program crashes if this is null.

class A
{
    function test(s32 v){}
}

function main(): s32
{
    var A* a;
    a.test(0); // runtime error, member function called with null `this`
    return 0;
}

CastExpression§

Grammar§

CastExpression ::= PostExpression ":" Type

Semantics§

The cast expression handles several types of conversion.

numeric truncation§

Expressions that have an integral or a floating point type can be cast to values that have for an integral or floating point type of smaller size.

const s8 a = 4096:s8;  // int literals are u64
const f32 b = 0.0:f32; // float literals are f64

numeric extension§

Expressions that have an integral or a floating point type can be cast to wider integral or floating point values. This cast is usually only implicit.

const s32 a = 2:s8;  // u64 (trunc) s8 (implicit ext) s32

floating point type to integral type§

Expressions that have a floating point types can be converted to integral values, i.e the integer part is taken. The opposite is also possible but just like for numeric extension, it is done implictly.

const s32 a = 1.5:s32; // 1

overload extraction§

A cast on an IdentExpression that resolves to an OverloadDeclaration has for effect to extract the function whose type matches to the target type.

@foreign function puts(s8* str): s32;

static function one(s32 p1):s32         { return p1;        }
static function two(s32 p1; s32 p2):s32 { return p1 + p2;   }

overload O
{
    one,
    two
}

function main(): s32
{
    var auto a = O:(static function(s32 p1): s32))(1);  // extract "O.one" and call it
    assert(a == 1);
    return 0;
}

The extraction is done during compilation so it is an error to try to cast to a type that’s not represented in the set.

bit casts§

Bit casts, because of their memory corruption proneness, are only allowed using pointers and dereferences

struct S1 {}
struct S2 {}

var S1 s1;
var S2 s2;

s1 = s2:S1;     // error, illegal cast
s1 = *(&s2:S1*);// ok, if you insist

class casts§

If the expression type and the target type are both a pointer to a class then in a first time, the inheritance list of the first class type is inspected. If it contains the target class type then the cast is a simple reinterpret cast.

Otherwise then a dynamic cast is performed. A dynamic cast consists of a comparison of the address of the virtual table of the class instance given by the expression to the address of the virtual table of the class given by the target type.

Dynamic casts are only allowed if the target type contains virtual fonctions, i.e if it has a non null virtual table.

Dynamic casts are not allowed on stack allocated classes.

DotExpression§

Grammar§

DotExpression ::= PostExpression "?"? "." PostExpression

Semantics§

The expression allows the access to a member of the aggregate given by the left hand side. The expression takes the type of the right hand side, i.e the member type.

Special cases are allowed where the right hand side does not really give a member. The following sections describes how resolving works.

The aggregate§

First the left had side is resolved to either a declaration or a type. This gives a scope that is used to solve the right hand side.

Properties§

Before handling the right hand side, builtin properties are tried. Note that this means that builtin properties take the precedence over members that would have the same name.

.decref§

This void property has for effect to decrement the reference count of the TypeRcArray given by the dot left hand side.

It is designed to manage aggregate members and globals, not local rc arrays.

.incref§

This void property has for effect to increment the reference count of the TypeRcArray given by the dot left hand side.

It is designed to manage aggregate members and globals, not local rc arrays.

.refcount§

The property yields a usize that gives the reference count of the TypeRcArray given by the dot left hand side.

.sizeof§

The property yields a s32 that gives the byte size of the dot left hand side symbol.

assert(u64.sizeof == 8);

.stringof§

The property yields a s8* null terminated string that represents the source code, normalized, of the dot left hand side.

@foreign function puts(s8* str): s32;
puts((a+b*x).stringof); // prints `(a + b * x)`

.length§

The property yields a usize value that is the length of the array represented by the left hand side.

s32[] array;
assert(array.length == 0);

When this property is used as the left hand side of an AssignExpression then the property is transformed into an internal expression that has for effect to set the array length. This internal expression is comparable to a call to a function taking a var array and a usize length as parameters.

s32[+] array;
array.length += 1; // expand array memory and set its new length

is conceptually like

function setLength(var s32[+] a; const usize len);
s32[+] array;
setLength(array, array.length + 1);

.ptr§

The property yields a rvalue pointer to the first element of the array represented by the left hand side.

s32[] array;
assert(array.ptr == null);

as the property yields a pointer, bounds check are bypassed

u8[+] array;
array.length += 1;
array.ptr[7]  = 8; // may crash or not, depending on the memory allocator implementation
array[7]      = 8; // guaranteed to crash if bounds checks are enabled

.min and .max§

If the left hand side type is integral then the property yields an IntegerExpression that has for value either the min or max value that a variable of this type can represent.

.reverse§

This property is only valid when used on a the expression that gives a ForeachStatement iterable.

Automatic dereference§

Then just before taking the most straight path to resolve the right hand side, the left hand side is inspected. If its type is a pointer to an aggregate then the scope is set to its target.

struct S { var s64 member; }

function test(S* s)
{
    s.member = 8; // just like (*s).member = 8;
}

Accessing aggregate members always requires the aggregate to be a Lvalue (that is fundamentally a pointer) anyway.

Straight path§

Then the regular path is borrowed from the scope given by the left hand side, e.g to solve enum members, struct members, etc.

Uniform Function Call§

In the context of a CallExpression and if the straight path does not allow to resolve to a valid member then the scope given by the left hand side is ignored and solving is tried again. In case of success the left hand side becomes the first parameter of the call.

struct S { var s64 member; }

function test1(S  s) {}
function test2(S* s) {}

function test()
{
    var S s;
    s.test1(); // `S` does not contain a `test1` member
               // `test1` can be resolved from the current scope
               // to a function that resides in the parent scope
               // `test1(s)` will be tried instead

    var S* ps = &s;
    ps.test2(); // auto dereference works as well
}

lvalue parameter pipes§

Still in the context of a CallExpression, if the dot left hand side is a call to a void function, that the first parameter is for a lvalue parameter then this parameter is tried as first parameter of function being called in the dot right hand side.

unit pipe_lvalue_param;

function one(var s32 a){}
function two(var s32 a){}
function three(var s32 a): s32{return a + 1;}

function main(): s32
{
    var s32 a = 42;
    // same as one(a); two(a); var s32 b = three(a);
    var s32 b = one(a).two().three();
    return 0;
}

Optional access§

If the left hand side is followed by a question mark then the full evaluation of the chain becomes optional. An optional chain must verifies that both the the left and right hand side are evaluable as NullExpressions. Even when shortcut, an optional chain always yields a value that has the same type as the right hand side.

struct Second   { var s8* str;        // nullable, ok }
struct First    { var Second* second; // nullable, ok }

var First* first;  // nullable, ok
var s8* str = first?.second?.str;
assert(str == null);

All other PostExpressions, even when they yields nullable values, are not supported. For example the CallExpression

struct Second   { function call():s8* {return null;}   }
struct First    { var Second* second;                  }

var First* first;
var s8* str = first?.second?.call(); // error

Instead an IfElseStatement must be used

var First* first;
if var auto s = first?.second do s.call();

IndexExpression§

Grammar§

IndexExpression ::= PostExpression "[" Expression ("," Expression)* "]"

Semantics§

The first expression must resolve to a symbol that supports indexing, which includes

For those types a single index is allowed and it must be of an integer type. Multiple indexes are only supported by operator overloads.

When bounds checks are active, and if it does not resolve to a pointer, the expression that gives the index is verified before reading the element.

Enums are only iterable if ordered (e.g usable to define a TypeEnumSet) and if not ordered then only if they do not extend another enum.

PostDecrementExpression§

Grammar§

PostDecrementExpression ::= PostExpression "--"

Semantics§

the result of the post decrement is the value of the nested expression and as a side effect, the stored value of the nested expression is decremented. The result is evaluated before the side effect.

The nested expression must be a lvalue of type integral, floating point or pointer.

The expression takes the type of its nested expression.

PostIncrementExpression§

Grammar§

PostIncrementExpression ::= PostExpression "++"

Semantics§

the result of the post increment is the value of the nested expression and as a side effect, the stored value of the nested expression is incremented. The result is evaluated before the side effect.

The nested expression must be a lvalue of type integral, floating point or pointer.

The expression takes the type of its nested expression.

SliceExpression§

Grammar§

SliceExpression ::= PostExpression "[" RangeExpression? "]"

Semantics§

The first expression must resolve to a symbol that supports indexing, which includes

The optional range expression gives the lower and upper (exclusive) indexes of the slice. When not specified the lower index is set to 0 and the upper index to the first expression .length property.

function tabCount(s8[] str): auto
{
    var usize result;
    while str.length do
    {
        result += str[0] == 9;
        str = str[1 .. str.length];
    }
    return result;
}

When bounds checks are active, and if it does not resolve to a pointer, the expressions that give the slice bounds are checked.

Slices neither allocate nor own memory, they only gives a view. Slices have their own specific type, see TypeSlice.