Post Expressions

Grammar§

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

CallExpression§

Grammar§

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

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.

Calls yield rvalues unless

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 expression list between parens represents 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.

Tuple arguments§

If an argument does not match and if its type is TypeTuple then the argument is expanded.

function f(u64 a; f64 b);
var auto a = (0, 0.0);
f(a);

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 that has the matching type.

@foreign function puts(char* 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;
}

If the extraction fails then the cast returns a NullExpression.

@overload(o) function f(){}

function test()
{
    alias T = function(s32 _);
    assert(!echo(isFunction, o:T));
}

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.

void cast§

If an expression is cast to TypeVoid then its side-effect is ignored

function f()
{
    return 1:();
}

DotExpression§

Grammar§

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

Semantics§

The expression allows the access to a member of the aggregate given by the LHS. The expression takes the type of the RHS, i.e the member type.

Special cases are allowed where the RHS 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 RHS.

Properties§

Before handling the RHS, 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 LHS.

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 LHS.

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 LHS.

.sizeof§

The property yields a s32 that gives the byte size of the dot LHS symbol.

assert(u64.sizeof == 8);

.stringof§

The property yields a char* null terminated string that represents the source code, normalized, of the dot LHS.

@foreign function puts(char* 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 LHS.

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

When this property is used as the LHS 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.

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

is conceptually like

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

.ptr§

The property yields a rvalue pointer to the first element of the array represented by the LHS.

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

as the property yields a pointer, bounds check are bypassed

var 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 LHS 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.

.expand§

If the LHS type is a TypeTuple then the tuple is expanded.

The property is supported where a comma-separated list of expressions is possible, this includes

the content of a TupleExpression

var auto te1 = (0, (1,2).expand);
assert(te1 == (0, 1, 2));

the arguments of a CallExpression

function f(u64 a; u64 b);
var auto v = (0:u64, 1:u64);
f(v.expand);

and the items of an ArrayExpression

var auto t = (1,2);
var u32[3] a = [t.expand,3];
assert(a == [1,2,3]);

.tupleof§

If the LHS is a struct then returns a TupleExpression that consists of all the non-static members that are VariableDeclarations.

struct S
{
    var u64 a = 1;
    var u64 b = 2;
}

var S s;
var (u64,u64) ab = s.tupleof;
assert(ab == (1,2));

If the LHS is a ParenExpression then returns a TupleExpression made of the nested expression.

Automatic dereference§

Then just before taking the most straight path to resolve the RHS, the LHS 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;
}

Access to an aggregate member 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 LHS, 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 LHS is ignored and solving is tried again. In case of success the LHS 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
}

dot var checks§

If the runtime check “dotvars” is enabled and if the LHS is an aggregate instance then the LHS is verified before reading the RHS and the program crashes if the LHS is null.

Given how frequently the check is generated, emitting the matching code is costly. For that reason the check is not generated if the compiler can determine that the LHS will not be null:

Additionally it’s also possible to bypass manually the check

var a1 = new A;
a1.member1 = value1; // a1 is checked
a1.member2 = value2; // a1 is checked

var a2 = new A;
if a2 do
{
    a2.member1 = value1; // a2 is not checked
    a2.member2 = value2; // a2 is not checked
}

Optional access§

If the LHS is followed by a question mark then the full evaluation of the chain can be skipped.

The LHS type must be a evaluable as a condition.

If the LHS evaluates to false then the DotExpression yields a default-initialized value with the same type and value category as the RHS.

Otherwise, the DotExpression yields the RHS.

Optional access is restricted to fields access and function calls.

Optional access to fields§

struct Second   { var char* str;       }
struct First    { var Second* second;  }

var First* first;
var char* str = first?.second?.str;
assert(str == null);

struct Value    { var s32 m = 1; }

var Value value1;
assert((&value1)?.m == 1);  // 1: static init defined in `Value`
var Value* value2;
assert(value2?.m == 0);     // 0: `s32` default value

Optional calls§

As the callee is given by a DotExpression this is only possible on member function and free functions called with the UFC syntax.

struct S
{
    function memberFunc(): u8* { return null; }
}
var S* s;
assert(!s?.memberFunc());

function pseudoMemberFunc(u8* p): u8* { return null; }
assert(!(null:u8*)?.pseudoMemberFunc());

Optional calls can yield TypeVoid

struct S
{
    function voidFunc(u8* p) {}
}
var S* s;
s?.voidFunc(null);

Extra parameters are only evaluated if the optional expression yields true

struct S
{
    function test(u8* p): u8* {return null;}
}
function getXtraParam(): u8* {assert(0);}
var S* s;
s?.test(getXtraParam()); // getXtraParam not called, no runtime crash

Optional access on value types§

Using an @operator(true) function, value types become suitable for optional accesses.

struct S
{
    var u8 accept;
    var u64 a = 123;
    @operator(true) function toCondtion(): bool
    {
        return (accept++ & 1) == 0;
    }
}

function test()
{
    var S s;
    var auto a = s?.a; // a = s.toCondition() ? s.a else 0:
    assert(a == 123);
    var auto b = s?.a;
    assert(b == 0);
}

Effect on the runtime checks§

nullthis and dotvars checks are not generated for DotExpressions where the LHS is an OptAccessExpression.

IndexExpression§

Grammar§

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

Semantics§

Multiple indexes are only supported by operator overloads, in which case the following rules dont apply.

A single index is allowed.

The type of the index must be integral.

The index is evaluated first.

The unary operand must resolve to a symbol that supports indexing, which includes

When bounds checks are active and if the unary operand gives an array then the index is verified before reading the element.

Bounds are statically checked if the index resolve to an IntegerExpression and that

Otherwise the check is performed at run-time.

If the unary operand resolves to an enum set then it is only accepted either if

If the unary operand resolves to a TypeTuple then

If the unary operand gives a Type and that this type does not implements an @operator(a[b]) function then the expression is lowered to a TypeExpression wrapping a TypeStaticArray.

PostDecrementExpression§

Grammar§

PostDecrementExpression ::= PostExpression "--"

Semantics§

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

The unary operand must be a lvalue of type integral, floating point or pointer.

The expression takes the type of the unary operand.

PostIncrementExpression§

Grammar§

PostIncrementExpression ::= PostExpression "++"

Semantics§

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

The unary operand must be a lvalue of type integral, floating point or pointer.

The expression takes the type of its unary operand.

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(char[] 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.

If the range is not specified and if the unary operand gives a Type and that this type does not implement an @operator(a[]) function then the expression is lowered to a TypeExpression wrapping a TypeSlice.

Similarily, if the + is present then the expression is lowered to a [RcArrayType].