3.7. Expressions

3.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 src = new Foo()
var dst : Foo?
dst <- src          // dst now points to the Foo instance, src 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 src : array<int>
var dst : array<int>
dst := src          // dst is now a deep copy of src

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

3.7.2. Operators

3.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).

3.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") }

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

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

3.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           // single-expression block — arrow shorthand

apply_twice(3) @(a : int) => a * 2   // lambda — @ is still required

This shorthand — called assumed pipe — works with all three call forms:

  • Named function callsfunc()

  • Dot-method callsobj.method()

  • Arrow-method callsobj->fn()

// dot-method call with assumed pipe
class Receiver {
    def call_method(blk : block<() : int>) : int {
        return invoke(blk)
    }
}
var r = new Receiver()
let res = r.call_method() $ : int {    // same as r.call_method() <| $ : int { ... }
    return 42
}

// arrow-method call with assumed pipe
var c : Callable                       // struct with a lambda field
c->fn() $ : int {                      // same as c->fn() <| $ : int { ... }
    return 77
}

It also works with lambdas (@) and no-capture lambdas (@@), and with the => single-expression shorthand — arr |> sort() $(a, b) => a < b needs no <|. The explicit <| is still needed when a non-block expression is piped as the last argument (e.g. take() <| new Node(v = 5)).

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"
}

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

3.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 higher precedence than the bitwise | operator (see the precedence table below).

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

3.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    // the assignment targets the ?? dummy fallback lvalue, so writes land in dummy when navigation yields null

3.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).

3.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")
    }
}

3.7.2.12. Cast, Upcast, and Reinterpret

cast performs a safe upcast from a derived structure type to a parent (base) 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)
}

3.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)

3.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?
}

addr<T?>(x) takes the address and reinterprets it as T? in one step — pure syntax sugar for reinterpret<T?>(addr(x)). A single unsafe() covers both halves, where the spelled-out form needs two:

var v = float3(1., 2., 3.)
let p = unsafe(addr<float?>(v))                         // one unsafe gate
// equivalent long form, two gates:
// let q = unsafe(reinterpret<float?>(unsafe(addr(v))))

The target type must be a pointer — addr<int>(x) is a compile error (it is always a typo for addr<int?>(x)).

3.7.2.15. Original Operator Access (!)

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

Placing ! in front of an overloadable access or test operator gives the original operator, never the overload. a!.x bypasses any . operator overloading (a property, see Classes) and accesses the underlying field directly. This is useful when a type defines a custom . operator but you need to reach the actual field:

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

The whole family follows the same rule. Each spelling is the overloadable operator with ! in front, and yields the built-in behavior even when a matching overload (or a variant macro) exists:

Raw spelling

Original operation

a!.x

field access (skips operator .)

a!?.x

null-safe field access (skips operator ?.)

a![i]

indexing (skips operator [])

a!?[i]

null-safe indexing (skips operator ?[])

a !?? b

pointer null-coalescing (skips operator ??)

a !is x

variant / type check (skips operator is)

a !as x

variant access (skips operator as)

a !?as x

safe variant access (skips operator ?as)

Generated and generic code uses these forms to guarantee the language’s own semantics no matter what overloads a user module brings into scope.

The older dot-prefixed spellings remain valid and equivalent: a. .x (the two dots must be separated by a space — .. with no space is the interval operator token, so sp..payload parses as interval(sp, payload)), a.?.x, a.[i], and a.?[i]. The variant and coalescing operators have no dot-prefixed form — ! is the only bypass spelling there.

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

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

3.7.2.18. Operators Precedence

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

highest

|>  <|

is  as

-  +  ~  !   ++  --

??

/  *  %

+  -

<<  >> <<< >>>

<  <=  >  >=

==  !=

&

^

|

&&

^^

||

?  :

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

..  =>

,

lowest

3.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).

3.7.4. Struct, Class, and Handled Type Initializer

Structures can be initialized by specifying field values:

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

let a = Point(x = 13, y = 11)                   // x = 13, y = 11
let b = Point(x = 13)                           // x = 13, y = 2 (default)
let c = unsafe(Point(uninitialized x = 13))     // x = 13, y = 0 (uninitialized construction requires unsafe)

Arrays of structures can be constructed inline:

var arr <- array struct<Point>((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).

3.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).

3.7.6. Variant Initializer

Variants are created by specifying exactly one field:

variant Number {
    i : int
    f : float
}

let ni = Number(i = 3)
let nf = Number(f = 4.0)

Variants can also be declared as type aliases:

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

(see Variants).

3.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).

3.7.8. default and new

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

var a = default<Point>          // all fields zeroed, then default initializer called
var b = unsafe(default<Point> uninitialized)  // all fields zeroed, no initializer (uninitialized requires unsafe)

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

var p = new Point()             // Point? pointer, default initialized
var q = new Point(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>

3.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).

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