2.6. Statements

A Daslang program is a sequence of statements. Statements in Daslang are comparable to those in C-family languages (C/C++, Java, C#, etc.): assignments, function calls, control flow, and declarations. There are also Daslang-specific statements such as with, assume, static_if, and generators.

Statements can be separated with a newline or a semicolon ;.

2.6.1. Visibility Block

A sequence of statements delimited by curly brackets is called a visibility block. Variables declared inside a visibility block are only visible within that block:

def foo {
    var x = 1           // x is visible here
    {
        var y = 2       // y is visible here
        x += y
    }
    // y is no longer visible
}

2.6.2. Control Flow Statements

2.6.2.1. if/elif/else

Conditionally execute a block depending on the result of a boolean expression:

if ( a > b ) {
    a = b
} elif ( a < b ) {
    b = a
} else {
    print("equal\n")
}

Daslang has a strong boolean type. Only expressions of type bool can be used as conditions.

One-liner if syntax:

A single-expression if statement can be written on one line:

if ( a > b ) { a = b }

Postfix if syntax:

The condition can be written after the statement:

a = b if ( a < b )

Ternary-style if:

A full ternary expression can use if/else inline:

return 13 if ( a == 42 ) else return 7

2.6.2.2. static_if / static_elif

static_if evaluates its condition at compile time. It is used in generic functions to select code paths based on type properties. Branches that do not match are removed from the compiled output, and do not need to be valid for the given types:

def describe(a) {
    static_if ( typeinfo(is_pointer type<a>) ) {
        print("pointer\n")
    } static_elif ( typeinfo(is_ref_type type<a>) ) {
        print("reference type\n")
    } else {
        print("value type\n")
    }
}

Unlike regular if, static_if does not require its condition to be of type bool — it only requires a compile-time constant expression.

static_if is the primary mechanism for conditional compilation in generic code (see Generic Programming).

2.6.2.3. while

Execute a block repeatedly while a boolean condition is true:

var i = 0
while ( i < 10 ) {
    print("{i}\n")
    i++
}

while ( true ) {
    if ( done() ) {
        break
    }
}

The condition must be of type bool.

2.6.3. Ranged Loops

2.6.3.1. for

Execute a block once for each element of one or more iterable sources:

for ( i in range(0, 10) ) {
    print("{i}\n")          // prints 0 through 9
}

Multiple iterables can be traversed in parallel. Iteration stops when the shortest source is exhausted:

var a : array<int>
var b : int[10]
resize(a, 4)
for ( l, r in a, b ) {
    print("{l} == {r}\n")   // iterates over 4 elements (length of a)
}

Table keys and values can be iterated using the keys and values functions:

var tab <- { "one"=>1, "two"=>2 }
for ( k, v in keys(tab), values(tab) ) {
    print("{k}: {v}\n")
}

Iterable types include ranges, arrays, fixed arrays, tables (via keys/values), strings (via each), enumerations (via each_enum), and custom iterators (see Iterators).

Any type can be made directly iterable by defining an each function that accepts the type and returns an iterator. When such a function exists, for (x in y) is equivalent to for (x in each(y)):

struct Foo {
    data : array<int>
}

def each ( f : Foo ) : iterator<int&> {
    return each(f.data)
}

var f = Foo(data <- [1, 2, 3])
for ( x in f ) {           // calls each(f) automatically
    print("{x}\n")
}

Tuple expansion in for loops:

When iterating over containers of tuples, elements can be unpacked directly:

var data <- [(1, 2.0, "three"), (4, 5.0, "six")]
for ( (a, b, c) in data ) {
    print("{a} {b} {c}\n")
}

2.6.4. break

Terminate the enclosing for or while loop immediately:

for ( x in arr ) {
    if ( x == target ) {
        break
    }
}

break cannot cross block boundaries. Using break inside a block passed to a function is a compilation error.

2.6.5. continue

Skip the rest of the current loop iteration and proceed to the next one:

for ( x in range(0, 10) ) {
    if ( x % 2 == 0 ) {
        continue
    }
    print("{x}\n")      // prints only odd numbers
}

2.6.6. return

Terminate the current function, block, or lambda and optionally return a value:

def add(a, b : int) : int {
    return a + b
}

All return statements in a function must return the same type. If no expression is given, the function is assumed to return void:

def greet(name : string) {
    print("Hello, {name}!\n")
    return
}

Move-on-return transfers ownership of a value using the <- operator:

def make_array : array<int> {
    var result : array<int>
    result |> push(1)
    result |> push(2)
    return <- result
}

In generator blocks, return must always return a boolean expression where false indicates the end of generation (see Generators).

2.6.7. yield

Output a value from a generator and suspend its execution until the next iteration. yield can only be used inside generator blocks:

var gen <- generator<int>() <| $ {
    yield 0
    yield 1
    return false    // end of generation
}

Move semantics are also supported:

yield <- some_array

(see Generators).

2.6.8. pass

An explicit no-operation statement. pass does nothing and can be used as a placeholder in blocks that are intentionally empty:

def todo_later {
    pass
}

2.6.9. finally

Declare a block of code that executes when the enclosing scope exits, regardless of how it exits (normal flow, break, continue, or return):

def test(a : array<int>; target : int) : int {
    for ( x in a ) {
        if ( x == target ) {
            return x
        }
    }
    return -1
} finally {
    print("search complete\n")
}

finally can be attached to any block, including loops:

for ( x in data ) {
    if ( x < 0 ) {
        break
    }
} finally {
    print("loop done\n")
}

A finally block cannot contain break, continue, or return statements.

The defer macro from daslib/defer provides a convenient way to add cleanup code to the current scope’s finally block:

require daslib/defer

def process {
    var resource <- acquire()
    defer() {
        release(resource)
    }
    // ... use resource ...
}   // release(resource) is called here

Multiple defer statements execute in reverse order (last-in, first-out).

The defer_delete macro adds a delete statement for its argument without requiring a block.

2.6.10. Local Variable Declarations

Local variables can be declared at any point inside a function. They exist from the point of declaration until the end of their enclosing visibility block.

let declares a read-only (constant) variable, and var declares a mutable variable:

let pi = 3.14159        // constant, cannot be modified
var counter = 0         // mutable, can be modified
counter++

Variables can be initialized with copy (=), move (<-), or clone (:=) semantics:

var a <- [1, 2, 3]      // move: a now owns the array
var b : array<int>
b := a                  // clone: b is a deep copy of a

If a type is specified, the variable is typed explicitly. Otherwise, the type is inferred from the initializer:

var x : int = 42        // explicit type
var y = 42              // inferred as int
var z : float           // explicit type, initialized to 0.0

inscope variables:

When inscope is specified, a delete statement is automatically added to the finally section of the enclosing block:

var inscope resource <- acquire()
// ... use resource ...
// delete resource is called automatically at end of block

inscope cannot appear directly in a loop block, since the finally section of a loop executes only once.

Variable name aliases (aka):

The aka keyword creates an alternative name (alias) for a variable. Both names refer to the same value. This works in let, var, and for declarations:

var a aka alpha = 42
print("{alpha}\n")           // prints 42 — alpha is the same variable as a

for (x aka element in [1,2,3]) {
    print("{element}\n")    // element is the same as x
}

Tuple expansion:

Variables can be unpacked from tuples:

var (x, y, z) = (1, 2.0, "three")
// x is int, y is float, z is string

2.6.11. assume

The assume statement creates a named alias for an expression without creating a new variable. Every use of the alias substitutes the original expression:

var data : array<array<int>>
assume inner = data[0]
inner |> push(42)       // equivalent to data[0] |> push(42)

assume is particularly useful for simplifying repeated access to nested data:

assume cfg = settings.graphics.resolution
print("width={cfg.width}, height={cfg.height}\n")

Note

assume does not create a variable — it creates a textual substitution. The expression is re-evaluated at each point of use.

2.6.12. with

The with statement brings the fields of a structure, class, or handled type into the current scope, allowing them to be accessed without a prefix:

struct Player {
    x, y : float
    health : int
}

def reset(var p : Player) {
    with ( p ) {
        x = 0.0
        y = 0.0
        health = 100
    }
}

Without with, the same code would require the p. prefix on each field access.

Multiple with blocks can be nested. If field names conflict, the innermost with takes precedence.

2.6.13. delete

The delete statement invokes the finalizer for a value, releasing any resources it holds. After deletion, the value is zeroed:

var arr <- [1, 2, 3]
delete arr              // arr is now empty

Deleting pointers is an unsafe operation because other references to the same data may still exist:

var p = new Foo()
unsafe {
    delete p            // p is set to null, memory is freed
}

(see Finalizers).

2.6.14. Function Declaration

Functions are declared with the def keyword:

def add(a, b : int) : int {
    return a + b
}

def greet {
    print("hello\n")
}

(see Functions for a complete description of function features).

2.6.15. try/recover

Enclose a block of code that may trigger a runtime panic, such as null pointer dereference or out-of-bounds array access:

try {
    var p : Foo?
    print("{*p}\n")         // would panic: null pointer dereference
} recover {
    print("recovered from panic\n")
}

Warning

try/recover is not a general error-handling mechanism and should not be used for control flow. It is designed for catching runtime panics (similar to Go’s recover). In production code, these situations should be prevented rather than caught.

2.6.16. panic

Trigger a runtime panic with an optional message:

panic("something went very wrong")

The panic message is available in the runtime log. A panic can be caught by a try/recover block.

2.6.17. label and goto

Daslang supports numeric labels and goto for low-level control flow:

label 0:
    print("start\n")
    goto label 1
label 1:
    print("end\n")

Labels use integer identifiers. Computed goto is also supported:

goto label_expression

Warning

Labels and goto are low-level constructs primarily used in generated code (such as generators). They are generally not recommended for regular application code.

2.6.18. Expression Statement

Any expression is also valid as a statement. The result of the expression is discarded:

foo()               // function call as statement
a + b               // valid but result is unused
arr |> push(42)     // pipe expression as statement

2.6.19. Global Variables

Global variables are declared at module scope with let (constant) or var (mutable):

let MAX_SIZE = 1024
var counter = 0

Global variables are initialized once during script initialization (or each time init is manually called on the context).

shared indicates that the variable’s memory is shared between multiple Context instances and initialized only once:

let shared lookup_table <- generate_table()

private indicates the variable is not visible outside its module:

var private internal_state = 0

(see Constants & Enumerations for more details).

2.6.20. enum

Declare an enumeration — a set of named integer constants:

enum Color {
    Red
    Green
    Blue
}

(see Constants & Enumerations).

2.6.21. typedef

Declare a type alias:

typedef Vec3 = float3
typedef IntPair = tuple<int; int>

Type aliases can also be declared locally inside functions or structure bodies (see Type Aliases).

See also

Expressions for expression syntax and operators, Functions for function declarations using def, Iterators for for loop iteration patterns, Generators for yield in generator functions.