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.