2.1. Program Structure

A daslang source file is a sequence of top-level declarations. This page describes the overall layout of a file and the key declarations that control how it interacts with the rest of the program.

2.1.1. File Layout

A typical daslang file follows this layout:

options gen2                              // compilation options

module my_module shared public            // module declaration

require math                              // imports
require daslib/strings_boost

struct MyData                             // type declarations
    value : int
    name : string

enum Color {
    red
    green
    blue
}

let MAX_COUNT = 100                       // global constants

var total : int = 0                       // global variables

typedef IntArray = array<int>             // type aliases

def helper(x : int) : int {               // functions
    return x * 2
}

[export]                                  // entry point
def main {
    print("hello\n")
}

The parser does not enforce a strict ordering among options, module, and require. However, the module declaration must appear before any type declarations (structs, enums, functions, global variables, type aliases). By convention, options lines come first, followed by module, then require.

Note

The order above is a convention, not a hard rule. The only enforced constraint is that module precedes type declarations.

2.1.2. Module Declaration

The module declaration names the current file’s module:

module my_module

If omitted, the module name defaults to the file name (without extension).

2.1.2.1. Modifiers

The module declaration supports several modifiers:

shared

Promotes the module to a built-in module. Only one instance is created per compilation environment, and it is shared across contexts:

module my_lib shared
public / private

Sets the default visibility of all declarations in the module. Functions, structs, enums, and globals inherit this default unless they specify their own visibility:

module my_lib public          // all declarations are public by default
module my_lib private         // all declarations are private by default

If neither is specified, the module uses the environment’s default (typically public).

inscope

Makes the module visible to all modules in the project without an explicit require. This uses the !inscope syntax:

module my_lib !inscope

Modifiers can be combined:

module my_lib shared public

2.1.3. Require Declaration

The require declaration imports another module:

require math
require daslib/ast_boost

Module names can contain / and . separators. The project is responsible for resolving module names into file paths.

2.1.3.1. Re-exporting

By default, required modules are private — they are only visible within the current module. The public modifier re-exports the module, making it transitively visible to any module that requires the current one:

require dastest/testing_boost public

2.1.3.2. Aliasing

When two modules share the same name, the as keyword provides a local alias:

require event
require sub/event as sub_event

def handle {
    sub_event::process()          // qualified call using the alias
}

(see Modules for details on module function visibility and the _ / __ module prefixes).

2.1.4. Options Declaration

The options declaration sets compiler options for the file:

options gen2
options no_unused_block_arguments = false

Multiple options can appear on one line, separated by commas:

options no_aot = true, rtti = true

A bare option name (without = value) is shorthand for = true:

options gen2          // equivalent to: options gen2 = true

(see Options for the complete list of recognized options).

2.1.5. Top-Level Declarations

After the header declarations, the rest of the file consists of:

All of these are peers in the grammar and can appear in any order, interleaved freely.

2.1.6. Visibility

Each top-level declaration can be marked public or private:

def public helper(x : int) : int {     // visible to other modules
    return x * 2
}

struct private Internal {               // only visible within this module
    data : int
}

If no visibility is specified, the declaration inherits the module’s default visibility.

Shared global variables use the shared keyword and are shared across cloned contexts:

let shared GLOBAL_TABLE : table<string; int>

2.1.7. Entry Points

A daslang program is compiled and simulated by the host application (a C++ executable). The host decides which functions to call. Several annotations mark functions with special roles.

2.1.7.1. [export]

Marks a function as callable from the host application. The host invokes exported functions by name through the context API:

[export]
def main {
    print("hello world\n")
}

There is nothing special about the name main — it is purely a convention. The host chooses which exported function(s) to call and in what order.

2.1.7.2. [init]

Marks a function to run automatically during context initialization. [init] functions cannot have arguments and cannot return a value:

[init]
def setup {
    print("initializing\n")
}

Multiple [init] functions execute in declaration order. Ordering can be controlled with attributes:

[init(tag="db")]
def init_database {
    pass
}

[init(after="db")]
def init_cache {
    pass
}

[init(before="db")]
def init_logging {
    pass
}

The option no_init disables all [init] functions.

2.1.7.3. [finalize]

Marks a function to run automatically during context shutdown. Same constraints as [init] — no arguments, no return value:

[finalize]
def cleanup {
    print("shutting down\n")
}

2.1.8. Expect Declaration

The expect declaration is used in test files to declare expected compilation errors. When present, the compiler treats the listed errors as intentional — the file compiles “successfully” only if exactly those errors (and no others) are produced.

This is primarily used in negative test suites to verify that the compiler correctly rejects invalid code:

expect 40214:3              // expect error 40214 exactly 3 times
expect 30304, 30101         // expect each error once (count defaults to 1)

The syntax is:

expect <error_code> [: <count>] [, <error_code> [: <count>] ...]

Multiple expect declarations can appear in the same file. Error codes are numeric identifiers organized by compilation phase:

Range

Category

10001–10011

Lexer errors (mismatched brackets, etc.)

20000–20001

Parser errors (syntax errors)

30101–30128

Semantic: invalid type/annotation/name

30201–30213

Semantic: already declared / too many args

30301–30311

Semantic: not found (type, func, var, etc.)

30401–30403

Semantic: can’t initialize

30501–30509

Semantic: can’t dereference/copy/move

30601–30602

Semantic: condition errors

31300

Unsafe operation outside unsafe block

39901–39903

Semantic: missing value/typeinfo

40101–40214

Lint-time errors and warnings

For example, a test that verifies the compiler rejects copying an array:

expect 30507    // cant_copy

[export]
def main {
    var a <- [1, 2, 3]
    var b = a           // error: can't copy array
}

2.1.9. Program vs. Module

The same file format is used for both programs and modules. The distinction is how the file is used:

Program (entry point)

The top-level file compiled by the host application. Typically contains [export] functions. May omit the module declaration.

Module (library)

A file imported via require by other files. Typically has a module declaration and provides types, functions, and globals for reuse.

A file can serve both roles simultaneously.

2.1.10. Execution Lifecycle

  1. The host compiles a source file into a Program

  2. The program is simulated into a Context

  3. The context is initialized:

    • Global variables are initialized in declaration order, per module

    • [init] functions run in declaration order (or topologically, if ordering attributes are used)

  4. The host calls [export] functions as needed

  5. The context is shut down:

    • [finalize] functions run

2.1.11. Complete Example

The following example shows a complete program with all structural elements:

options gen2

require math
require daslib/strings_boost

struct Particle {
    pos : float3
    vel : float3
    life : float
}

enum State {
    alive
    dead
}

let GRAVITY = float3(0.0, -9.8, 0.0)

var particles : array<Particle>

def update_particle(var p : Particle; dt : float) : State {
    p.vel += GRAVITY * dt
    p.pos += p.vel * dt
    p.life -= dt
    if (p.life > 0.0) {
        return State.alive
    }
    return State.dead
}

[init]
def setup {
    for (i in range(100)) {
        particles |> push(Particle(pos=float3(0), vel=float3(0, 10.0, 0), life=5.0))
    }
}

[export]
def main {
    let dt = 0.016
    for (p in particles) {
        update_particle(p, dt)
    }
    print("particles: {length(particles)}\n")
}

[finalize]
def cleanup {
    unsafe {
        delete particles
    }
}

Expected output:

particles: 100

See also

Modules for module declaration and require semantics, Options for compiler and runtime options, Annotations for [init], [finalize], and [export], Contexts for the execution context lifecycle, Constants and enumerations for global declarations.