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:
sharedPromotes 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/privateSets 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).inscopeMakes the module visible to all modules in the project without an explicit
require. This uses the!inscopesyntax: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:
Type aliases —
typedef, namedtuple,variant,bitfield(see Type Aliases)Enumerations —
enum(see Constants and Enumerations)Structures and classes —
struct,class(see Structs, Classes)Global variables —
let(constant) andvar(mutable) (see Constants and Enumerations)Functions —
def(see Functions)Top-level reader macros —
%macro_name~...~
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 |
|---|---|
|
Lexer errors (mismatched brackets, etc.) |
|
Parser errors (syntax errors) |
|
Semantic: invalid type/annotation/name |
|
Semantic: already declared / too many args |
|
Semantic: not found (type, func, var, etc.) |
|
Semantic: can’t initialize |
|
Semantic: can’t dereference/copy/move |
|
Semantic: condition errors |
|
Unsafe operation outside |
|
Semantic: missing value/typeinfo |
|
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 themoduledeclaration.- Module (library)
A file imported via
requireby other files. Typically has amoduledeclaration and provides types, functions, and globals for reuse.
A file can serve both roles simultaneously.
2.1.10. Execution Lifecycle
The host compiles a source file into a
ProgramThe program is simulated into a
ContextThe 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)
The host calls
[export]functions as neededThe 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.