2.7. Expressions

2.7.1. Assignment

Daslang provides three kinds of assignment:

Copy assignment (=) performs a bitwise copy of the value:

a = 10

Copy assignment is only available for POD types and types that support copying. Arrays, tables, and other container types cannot be copied — use move or clone instead.

Move assignment (<-) transfers ownership of a value, zeroing the source:

var b = new Foo()
var a : Foo?
a <- b              // a now points to the Foo instance, b is null

Move is the primary mechanism for transferring ownership of heavy types such as arrays and tables. Some handled types may be movable but not copyable.

Clone assignment (:=) creates a deep copy of the value:

var a : array<int>
a := b              // a is now a deep copy of b

Clone is syntactic sugar for calling the clone function. For POD types, clone falls back to a regular copy.

(see Move, Copy, and Clone for a complete guide, and Clone for detailed cloning rules).

2.7.2. Operators

2.7.2.1. Arithmetic

Daslang supports the standard arithmetic operators +, -, *, /, and % (modulo). Compound assignment operators +=, -=, *=, /=, %= and increment/decrement operators ++ and -- are also available:

a += 2          // equivalent to a = a + 2
x++             // equivalent to x = x + 1
--y             // prefix decrement

All arithmetic operators are defined for numeric and vector types (int, uint, int2int4, uint2uint4, floatfloat4, double).

2.7.2.2. Relational

Relational operators compare two values and return a bool result: ==, !=, <, <=, >, >=:

if ( a == b ) { print("equal\n") }
if ( x < 0 ) { print("negative\n") }

2.7.2.3. Logical

Logical operators work with bool values:

  • && — logical AND. Returns false if the left operand is false; otherwise evaluates and returns the right operand.

  • || — logical OR. Returns true if the left operand is true; otherwise evaluates and returns the right operand.

  • ^^ — logical XOR. Returns true if the operands differ.

  • ! — logical NOT. Returns false if the value is true, and vice versa.

Compound assignment forms are available: &&=, ||=, ^^=.

Important

&& and || use short-circuit evaluation — the right operand is not evaluated if the result can be determined from the left operand alone. Unlike their C++ counterparts, &&= and ||= also short-circuit the right side.

2.7.2.4. Bitwise Operators

Daslang supports C-like bitwise operators for integer types:

  • & — bitwise AND

  • | — bitwise OR

  • ^ — bitwise XOR

  • ~ — bitwise NOT (complement)

  • << — shift left

  • >> — shift right

  • <<< — rotate left

  • >>> — rotate right

Compound assignment forms: &=, |=, ^=, <<=, >>=, <<<=, >>>=:

let flags = 0xFF & 0x0F     // 0x0F
let rotated = value <<< 3   // rotate left by 3 bits

2.7.2.5. Pipe Operators

Pipe operators pass a value as the first (right pipe) or last (left pipe) argument to a function call:

  • |> — right pipe. x |> f(y) is equivalent to f(x, y)

  • <| — left pipe. f(y) <| x is equivalent to f(y, x)

def addX(a, b) {
    return a + b
}

let t = 12 |> addX(2) |> addX(3)   // addX(addX(12, 2), 3) = 17

Left pipe is commonly used to pass blocks and lambdas to functions:

def doSomething(blk : block) {
    invoke(blk)
}

doSomething() <| $ {
    print("hello\n")
}

In gen2 syntax a block or lambda that immediately follows a function call is automatically piped as the last argument, so the explicit <| can be omitted. Parameterless blocks also do not need the $ prefix:

doSomething() {                      // same as doSomething() <| $ { ... }
    print("hello\n")
}

build_string() $(var writer) {       // block with parameters — $ is still required
    write(writer, "hello")
}

sort(arr) @(a, b) => a < b           // lambda — @ is still required

This shorthand works with lambdas (@) and no-capture lambdas (@@) as well. The explicit <| is still needed when a block is passed to an expression (e.g. <| new ...) or for generator bodies (generator<T>() <| $() { ... }).

The lpipe macro from daslib/lpipe allows piping to the expression on the previous line:

require daslib/lpipe

def main {
    print()
    lpipe() <| "this is a string"
}

2.7.2.6. Interval Operator

The .. operator creates a range from two values:

let r = 1 .. 10     // equivalent to interval(1, 10)

By default, interval(a, b : int) returns range(a, b) and interval(a, b : uint) returns urange(a, b). Custom interval functions or generics can be defined for other types.

2.7.2.7. Null-Coalescing Operator (??)

The ?? operator returns the dereferenced value of the left operand if it is not null, otherwise returns the right operand:

var p : int?
let value = p ?? 42     // value is 42 because p is null

This is equivalent to:

let value = (p != null) ? *p : 42

?? evaluates expressions left to right until the first non-null value is found (similar to how || works for booleans).

The ?? operator has lower precedence than |, following C# convention.

2.7.2.8. Ternary Operator (? :)

The ternary operator conditionally evaluates one of two expressions:

let result = (a > b) ? a : b    // returns the larger value

Only the selected branch is evaluated.

2.7.2.9. Null-Safe Navigation (?. and ?[)

The ?. operator accesses a field of a pointer only if the pointer is not null. If the pointer is null, the result is null:

struct Foo {
    x : int
}

struct Bar {
    fooPtr : Foo?
}

def getX(bar : Bar?) : int {
    return bar?.fooPtr?.x ?? -1     // returns -1 if bar or fooPtr is null
}

The ?[ operator provides null-safe indexing into tables:

var tab <- { "one"=>1, "two"=>2 }
let i = tab?["three"] ?? 3      // returns 3 because "three" is not in the table

Both operators can be used on the left side of an assignment with ??:

var dummy = 0
bar?.fooPtr?.x ?? dummy = 42    // writes to dummy if navigation fails

2.7.2.10. Type Operators (is, as, ?as)

The is operator checks the active variant case:

variant Value {
    i : int
    f : float
}
var v = Value(i = 42)
if ( v is i ) { print("it's an int\n") }

The as operator accesses the value of a variant case. It panics if the wrong case is active:

let x = v as i      // returns 42

The ?as operator is a safe version of as that returns null if the case does not match:

let x = v ?as f ?? 0.0     // returns 0.0 because v is not f

These operators can also be used with classes and the is/as operator overloading mechanism (see Pattern Matching).

2.7.2.11. is type<T>

The is type<T> expression performs a compile-time type check. It returns true if the expression’s type matches the specified type, and false otherwise:

let a = 42
let b = 3.14
print("{a is type<int>}\n")     // true
print("{b is type<float>}\n")   // true
print("{b is type<int>}\n")     // false

This is useful in generic functions to branch on the actual type of a parameter:

def describe(x) {
    static_if (x is type<int>) {
        print("an integer\n")
    } static_elif (x is type<float>) {
        print("a float\n")
    } else {
        print("something else\n")
    }
}

2.7.2.12. Cast, Upcast, and Reinterpret

cast performs a safe downcast from a parent structure type to a derived type:

var derived : Derived = Derived()
var base : Base = cast<Base> derived

upcast performs an unsafe upcast from a base type to a derived type. This requires unsafe because the actual runtime type may not match:

unsafe {
    var d = upcast<Derived> base_ref
}

reinterpret reinterprets the raw bits of a value as a different type. This is unsafe and should be used with extreme caution:

unsafe {
    let p = reinterpret<void?> 13
}

2.7.2.13. Dereference

The * prefix operator dereferences a pointer, converting it to a reference. Dereferencing a null pointer causes a panic:

var p = new Foo()
var ref = *p        // ref is Foo&

The deref keyword can be used as an alternative:

var ref = deref(p)

2.7.2.14. Address-of

The addr function takes the address of a value, creating a pointer. This is an unsafe operation:

unsafe {
    var x = 42
    var p = addr(x)     // p is int?
}

2.7.2.15. Dot Operator Bypass

Smart pointers (smart_ptr<T>) are accessed the same way as regular pointers — using . for field access and ?. for null-safe field access.

The .. operator bypasses any . operator overloading and accesses the underlying field directly. This is useful when a handled type defines a custom . operator but you need to reach the actual field:

sp..x = 42      // accesses field x directly, skipping any . overload

2.7.2.16. Safe Index (?[)

The ?[ operator provides null-safe indexing. If the pointer or table key is null or missing, the result is null instead of a panic:

var tab <- { "one"=>1, "two"=>2 }
let i = tab?["three"] ?? 3      // returns 3 because "three" is not in the table

2.7.2.17. Unsafe Expression

Individual expressions can be marked as unsafe without wrapping an entire block:

let p = unsafe(addr(x))

This is equivalent to wrapping the expression in an unsafe { } block.

2.7.2.18. Operators Precedence

post++  post--  .   ->  ?. ?[ *(deref)

highest

|>  <|

is  as

-  +  ~  !   ++  --

??

/  *  %

+  -

<<  >> <<< >>>

<  <=  >  >=

==  !=

&

^

|

&&

^^

||

?  :

+=  =  -=  /=  *=  %=  &=  |=  ^=  <<=  >>=  <- <<<= >>>= &&= ||= ^^=  :=

..  =>

,

lowest

2.7.3. Array Initializer

Fixed-size arrays can be created with the fixed_array keyword:

let a = fixed_array<int>(1, 2)      // int[2]
let b = fixed_array(1, 2, 3)        // inferred as int[3]

Dynamic arrays can be created with several syntaxes:

let a <- [1, 2, 3]                  // array<int>
let b <- array(1, 2, 3)             // array<int>
let c <- array<int>(1, 2, 3)        // explicitly typed
let d <- [for x in range(0, 10); x * x]  // comprehension

(see Arrays, Comprehensions).

2.7.4. Struct, Class, and Handled Type Initializer

Structures can be initialized by specifying field values:

struct Foo {
    x : int = 1
    y : int = 2
}

let a = Foo(x = 13, y = 11)                     // x = 13, y = 11
let b = Foo(x = 13)                             // x = 13, y = 2 (default)
let c = Foo(uninitialized x = 13)                // x = 13, y = 0 (no default init)

Arrays of structures can be constructed inline:

var arr <- array struct<Foo>((x=11, y=22), (x=33), (y=44))

Classes and handled (external) types can also be initialized using this syntax. Classes and handled types cannot use uninitialized.

(see Structs, Classes).

2.7.5. Tuple Initializer

Tuples can be created with several syntaxes:

let a = (1, 2.0, "3")                                  // inferred tuple type
let b = tuple(1, 2.0, "3")                              // same as above
let c = tuple<a:int; b:float; c:string>(a=1, b=2.0, c="3")  // named fields

(see Tuples).

2.7.6. Variant Initializer

Variants are created by specifying exactly one field:

variant Foo {
    i : int
    f : float
}

let x = Foo(i = 3)
let y = Foo(f = 4.0)

Variants can also be declared as type aliases:

typedef Foo = variant<i:int; f:float>

(see Variants).

2.7.7. Table Initializer

Tables are created by specifying key-value pairs separated by =>:

var a <- { 1=>"one", 2=>"two" }
var b <- table("one"=>1, "two"=>2)      // alternative syntax
var c <- table<string; int>("one"=>1)    // explicitly typed

All values in a table literal must be of the same type. Similarly, all keys must be of the same type.

(see Tables).

2.7.8. default and new

The default expression creates a default-initialized value of a given type:

var a = default<Foo>            // all fields zeroed, then default initializer called
var b = default<Foo> uninitialized  // all fields zeroed, no initializer

The new operator allocates a value on the heap and returns a pointer:

var p = new Foo()               // Foo? pointer, default initialized
var q = new Foo(x = 13)         // with field initialization

new can also be combined with array and table literals to allocate them on the heap:

var p <- new [1, 2, 3]          // heap-allocated array<int>

2.7.9. typeinfo

The typeinfo expression provides compile-time type information. It is primarily used in generic functions to inspect argument types:

typeinfo(typename type<int>)        // returns "int" at compile time
typeinfo(sizeof type<float3>)       // returns 12
typeinfo(is_pod type<int>)          // returns true
typeinfo(has_field<x> myStruct)     // returns true if myStruct has field x

(see Generic Programming for a complete list of typeinfo traits).

2.7.10. String Interpolation

Expressions inside curly brackets within a string are evaluated and converted to text:

let name = "world"
print("Hello, {name}!")             // Hello, world!
print("1 + 2 = {1 + 2}")           // 1 + 2 = 3

Format specifiers can be added after a colon:

let pi = 3.14159
print("pi = {pi:5.2f}")            // formatted output

To include literal curly brackets, escape them with backslashes:

print("Use \{curly\} brackets")     // Use {curly} brackets

(see String Builder).

See also

Statements for control flow and variable declarations, Pattern matching for is/as/?as operator details, Datatypes for a list of types used in expressions.