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,
int2–int4, uint2–uint4, float–float4, 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. Returnsfalseif the left operand isfalse; otherwise evaluates and returns the right operand.||— logical OR. Returnstrueif the left operand istrue; otherwise evaluates and returns the right operand.^^— logical XOR. Returnstrueif the operands differ.!— logical NOT. Returnsfalseif the value istrue, 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 tof(x, y)<|— left pipe.f(y) <| xis equivalent tof(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.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
|
highest |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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.