daslang
Statically typed scripting language for games and real-time applications, by Gaijin
Entertainment. .das files run interpreted, AOT-compiled to C++, or LLVM-JIT-compiled; data
layout mirrors C++.
gen2 is the DEFAULT parser: a .das file is gen2 (C-like, braces) unless it opts out with
options gen2 = false - never infer gen1 (indentation-based) from a missing options gen2
marker. Never write gen1; by convention new files still open with options gen2. Everything
below is gen2.
References
Detailed semantics live in ./references/:
types.md - type catalog, vector lattice, literal suffixes, enums/variants/tuples/
bitfields, fixed arrays, the two const positions, temporary (#) and distinct types
functions.md - parameters, named arguments, overload resolution, operator overloading,
computed properties, pipes, precedence
structs-and-classes.md - initialization modes, virtuals, sealed/static, runtime type checks
closures.md - blocks/lambdas/function pointers, capture modes, generators, iterators
memory.md - the const model, move/copy/clone, finalizers, inscope, delete semantics,
contexts and threading, unsafe catalog
generics.md - auto(TT), type contracts, typeinfo traits, static_if, pattern matching
macros.md - compilation pipeline, macro classes, qmacro, qmatch, annotations
modules-and-stdlib.md - module declarations, require, with (module ...), options,
containers, daslib catalog
strings.md - the string surface, byte views, the parse family, build_string,
conversions, regular expressions
files-and-paths.md - fio helpers, directory walking, subprocess capture, glob patterns
json.md - sprint_json/sscan_json, JV, JsonValue?, safe navigation
queries.md - comprehensions, the linq surface, what fuses into one pass
cli-and-config.md - argv parsing, environment twins, config knobs
everything.md - generated digest of every module: what it is for, every public symbol,
each with a one-line description where the source carries one; search it by symbol or
intent before writing a helper, and read it whole only when auditing a file set for
duplicates
Functions
def add(a, b : int) : int { // `a, b : int` - one type for both names
return a + b
}
def twice(a) => a + a // arrow body; untyped param makes it generic
- Typed parameter groups separate with
, or ;.
- A parameter is const unless declared
var a : int.
- Return type follows
:, inferred when omitted; a => body's expression must start on the
same line as the =>.
- Named arguments follow positionals:
foo(pos, name = value).
- Defaults:
def greet(name : string = "world").
- Visibility is a prefix keyword -
def private helper(); there is no [private] annotation.
Variables and the const model
let a = 13 // immutable handle: type is `int const`
var b = 13 // mutable: type is `int`
var c : float = 0.5
let x = e means x : auto const = e - const lives entirely in the type, and dereference,
indexing and field access flow it onto the result, so a let handle gives const access to
everything reachable through it. Never strip const with reinterpret to write: the
optimizer trusts the const type and may silently delete the write.
Control flow
if (x > 0) {
r = 1
} elif (x < 0) {
r = -1
} else {
r = 0
}
for (i in range(10)) { ... } // 0..9
for (x in arr) { ... } // arrays, tables, iterators
while (running) { ... }
- Conditions must be
bool - no truthiness: if (ptr != null). if / for / while /
static_if all require the parentheses.
- Postfix conditionals:
return x if (cond), likewise break / continue.
- A bare
{ ... } at statement level is a lexical scope, and takes { ... } finally { ... }.
for (k, v in keys(tab), values(tab)) iterates in parallel.
Strings
- Strings are immutable; interpolate
{expr} as in "hello, {name}" - not ${} (compiles,
prints a literal $), not %s - with format specifiers "{value:08x}", escaping literal
braces "literal \{braces\}". Number to string: "{x}"
or string(42).
length(s) / empty(s) need no require; the rest of the string API needs
require strings.
int("123") does NOT exist; to_int (require strings) silently returns 0 on garbage.
External input goes through try_to_int / try_to_float from
require daslib/strings_convert, whose Result distinguishes invalid input from a real zero.
Pipes and block arguments
foo |> setXY(10, 11) // setXY(foo, 10, 11)
arr |> sort() $(a, b) => a < b // trailing block pipes as last argument
build_string() $(var writer) { // block with parameters
writer |> write("hello")
}
- A block/lambda immediately after a call is piped as its LAST argument ("assumed pipe"); a
parameterless block needs no
$: defer() { cleanup() }.
- Arrow shorthand also spells lambdas:
@(x) => x + 1 (capturing), @@(x) => x + 1
(no-capture function pointer).
a.foo(b) is sugar for foo(a, b) only when a is a struct/class value; primitives and
lambda-typedef values need |>.
Collections
var a <- [1, 2, 3] // array<int> literal (heap)
let f = fixed_array(1, 2, 3) // fixed-size int[3] (stack)
var t <- { "hp" => 100, "mp" => 50 } // table<string; int> literal
var s : table<string> // one type param = a SET
let v = t?["hp"] ?? 0 // safe lookup with default
var sq <- [for (x in range(10)); x * x] // array comprehension
var m <- { for (x in range(5)); x => x * x } // table comprehension
table[key] on a mutable table INSERTS a default entry when missing - read with
t?[key] ?? default, test with key_exists(t, k).
- Never do two
[] lookups on the same table in one expression - rehashing can invalidate
the first reference.
push copies, emplace moves (zeroes the source), push_clone deep-clones. Bulk forms
push_from / push_clone_from take a whole array at once.
Move, copy, clone
| Operator |
Effect |
= |
copy - value types (int, float, bool, string, pointers, POD structs) |
<- |
move - transfers ownership, source is zeroed |
:= |
clone - deep copy, source unchanged |
def make() : array<int> {
var r <- [1, 2, 3]
return <- r // moving return for non-copyable types
}
Structs are copyable only if all fields are. delete on a lambda requires unsafe - copies
alias one shared capture frame.
Pointers
var p = new Foo(x = 1) // heap allocation, type Foo?
let x = p?.x ?? 0 // safe navigation + null coalescing
var inscope q = new Foo() // auto-deleted at scope exit
unsafe {
delete p // manual delete is unsafe
}
T? is a nullable pointer with TWO const positions: Foo const? = const pointee,
Foo? const = const pointer. Writing through a parameter needs both non-const:
var p : Foo?.
addr(x) requires unsafe; safe_addr(x) yields a temporary pointer (T?#) without it.
Enums, variants, tuples, bitfields
enum Color {
red
green
}
let i = int(Color.red) // dot access; explicit cast to int
var t = (1, 2.0, "three")
let first = t._0 // access by index
let (x, y, z) = t // destructuring
bitfield Flags {
read
write
}
var fl : Flags = Flags.read | Flags.write
fl.write = false // single-bit assignment
if (fl.read) { ... } // single-bit test
Without the explicit : Flags, var fl = Flags.read | Flags.write infers an ANONYMOUS
bitfield type and the named single-bit access (fl.read) stops compiling.
Variants (tagged unions) are declared like structs, constructed with exactly one field
(Value(asInt = 42)), tested with is, read with as / ?as ... ?? default. Writing a
variant field or switching the active field requires unsafe.
Memory model
Locals and structs live on the stack; new, arrays and tables use the current context's
heap. Cleanup is deterministic and explicit - delete, var inscope, or moving ownership
out with <-; a plain local var arr : array<int> is NOT finalized at scope exit. Each
context (thread) owns its heap and cannot retain pointers into another's - clone what
crosses. delete on a container of pointers finalizes AND FREES every pointee; clear()
first when the pointers are non-owning.
Error handling
panic("msg"), and any runtime error (a failed bounds check, an error raised by a bound C++
function), unwinds to the nearest enclosing try { } recover { }; the recover block runs
and execution continues after it, in every execution tier. recover binds nothing - there is
no exception value to match on; the message is this_context().last_exception
(require daslib/rtti), with a trailing newline. Under the JIT the pair compiles to a
builtin call, and a return inside the try block is a compile error there (the
interpreter allows it). With no enclosing try the error unwinds out of the script: the
daslang CLI prints it and exits nonzero, an embedding host gets it back as a context error.
Never design APIs around panic-as-control-flow.
assert(cond, "msg") may be stripped in release; verify(cond, "msg") always runs. The
message must be a string CONSTANT - for a runtime-value diagnostic write
if (!cond) panic("bad n = {n}"). An asserted expression with side effects (invoking a
lambda counts) is rejected; use verify.
- Recoverable errors are values: return
bool, T?, or a variant with an error case.
{ } finally { } is SKIPPED on panic, by design - no must-run-on-failure cleanup there.
Callable types
| Kind |
Prefix |
Allocation |
Captures |
Storable |
Copyable |
| block |
$ |
stack |
surrounding scope by reference |
no |
no |
| lambda |
@ |
heap |
explicit modes: copy/move/clone/ref |
yes |
yes - pointer copy (alias) |
| function |
@@ |
none |
none |
yes |
yes |
@@name is a pointer to a named function.
Gotchas
- No implicit conversions between values.
float_var + int_var is a compile error;
cast one side. Bare integer LITERALS are the one exception, adapting to a known numeric
target - f + 1, d > 1, var f : float = 1, return 200 into uint8 - but NOT at
call arguments (take_f(1) fails when take_f wants float) or parameter defaults, and
float literals never adapt to double. No bool(int) - write x != 0. No string(bool)
- Hex literals are
uint - int(0x3F) when an int is needed.
- Reserved words that look like identifiers:
where, shared, label, expect,
pass, explicit, capture, deref, template are keywords; range, urange,
range64, urange64, block, function, lambda, iterator, and the small-vector type
names (half2, short4, byte16, ...) are type tokens. Using any as a
variable/parameter/field/function or annotation-argument name is a syntax error -
including @range = 5 on a field.
- A statement-level expression must fit one line unless wrapped in
(...): a
continuation line starting with + or - parses as a separate unary statement and is
silently optimized away.
options stack = N counts only in the MAIN module - a required library cannot raise
the program's stack.
require paths use /, resolve module mounts or same-directory names, and cannot
contain hyphens; a file elsewhere in the tree needs a relative path with the explicit
.das extension: require ../lib/util.das.
with (self) { x = 1 } brings struct fields into scope; assume alias = long.chain
makes a textual alias (re-evaluated each use, no copy).
Verified against daslang 0.6.4 (2026-08-08). Maintained in the daslang repository under
skills/daslang/; report errors there.
1---2name: daslang3description: daslang (formerly daScript) language reference - gen2 syntax, type system, memory model, generics, macros, standard library. Invoke whenever a task involves writing, reviewing, refactoring, debugging, or understanding .das files or a daslang project.4---56# daslang78Statically typed scripting language for games and real-time applications, by Gaijin9Entertainment. `.das` files run interpreted, AOT-compiled to C++, or LLVM-JIT-compiled; data10layout mirrors C++.1112**gen2 is the DEFAULT parser:** a `.das` file is gen2 (C-like, braces) unless it opts out with13`options gen2 = false` - never infer gen1 (indentation-based) from a missing `options gen2`14marker. Never write gen1; by convention new files still open with `options gen2`. Everything15below is gen2.1617## References1819Detailed semantics live in `./references/`:2021- `types.md` - type catalog, vector lattice, literal suffixes, enums/variants/tuples/22 bitfields, fixed arrays, the two const positions, temporary (`#`) and distinct types23- `functions.md` - parameters, named arguments, overload resolution, operator overloading,24 computed properties, pipes, precedence25- `structs-and-classes.md` - initialization modes, virtuals, sealed/static, runtime type checks26- `closures.md` - blocks/lambdas/function pointers, capture modes, generators, iterators27- `memory.md` - the const model, move/copy/clone, finalizers, `inscope`, delete semantics,28 contexts and threading, unsafe catalog29- `generics.md` - `auto(TT)`, type contracts, typeinfo traits, `static_if`, pattern matching30- `macros.md` - compilation pipeline, macro classes, `qmacro`, `qmatch`, annotations31- `modules-and-stdlib.md` - module declarations, `require`, `with (module ...)`, options,32 containers, daslib catalog33- `strings.md` - the string surface, byte views, the parse family, `build_string`,34 conversions, regular expressions35- `files-and-paths.md` - fio helpers, directory walking, subprocess capture, glob patterns36- `json.md` - `sprint_json`/`sscan_json`, `JV`, `JsonValue?`, safe navigation37- `queries.md` - comprehensions, the linq surface, what fuses into one pass38- `cli-and-config.md` - argv parsing, environment twins, config knobs39- `everything.md` - generated digest of every module: what it is for, every public symbol,40 each with a one-line description where the source carries one; search it by symbol or41 intent before writing a helper, and read it whole only when auditing a file set for42 duplicates4344## Functions4546```das47def add(a, b : int) : int { // `a, b : int` - one type for both names48 return a + b49}5051def twice(a) => a + a // arrow body; untyped param makes it generic52```5354- Typed parameter groups separate with `,` or `;`.55- A parameter is const unless declared `var a : int`.56- Return type follows `:`, inferred when omitted; a `=>` body's expression must start on the57 same line as the `=>`.58- Named arguments follow positionals: `foo(pos, name = value)`.59- Defaults: `def greet(name : string = "world")`.60- Visibility is a prefix keyword - `def private helper()`; there is no `[private]` annotation.6162## Variables and the const model6364```das65let a = 13 // immutable handle: type is `int const`66var b = 13 // mutable: type is `int`67var c : float = 0.568```6970`let x = e` means `x : auto const = e` - const lives entirely in the type, and dereference,71indexing and field access flow it onto the result, so a `let` handle gives const access to72*everything reachable through it*. Never strip const with `reinterpret` to write: the73optimizer trusts the const type and may silently delete the write.7475## Control flow7677```das78if (x > 0) {79 r = 180} elif (x < 0) {81 r = -182} else {83 r = 084}8586for (i in range(10)) { ... } // 0..987for (x in arr) { ... } // arrays, tables, iterators88while (running) { ... }89```9091- Conditions must be `bool` - no truthiness: `if (ptr != null)`. `if` / `for` / `while` /92 `static_if` all require the parentheses.93- Postfix conditionals: `return x if (cond)`, likewise `break` / `continue`.94- A bare `{ ... }` at statement level is a lexical scope, and takes `{ ... } finally { ... }`.95- `for (k, v in keys(tab), values(tab))` iterates in parallel.9697## Strings9899- Strings are immutable; interpolate `{expr}` as in `"hello, {name}"` - not `${}` (compiles,100 prints a literal `$`), not `%s` - with format specifiers `"{value:08x}"`, escaping literal101 braces `"literal \{braces\}"`. Number to string: `"{x}"`102 or `string(42)`.103- `length(s)` / `empty(s)` need no `require`; the rest of the string API needs104 `require strings`.105- `int("123")` does NOT exist; `to_int` (`require strings`) silently returns `0` on garbage.106 External input goes through `try_to_int` / `try_to_float` from107 `require daslib/strings_convert`, whose Result distinguishes invalid input from a real zero.108109## Pipes and block arguments110111```das112foo |> setXY(10, 11) // setXY(foo, 10, 11)113114arr |> sort() $(a, b) => a < b // trailing block pipes as last argument115116build_string() $(var writer) { // block with parameters117 writer |> write("hello")118}119```120121- A block/lambda immediately after a call is piped as its LAST argument ("assumed pipe"); a122 parameterless block needs no `$`: `defer() { cleanup() }`.123- Arrow shorthand also spells lambdas: `@(x) => x + 1` (capturing), `@@(x) => x + 1`124 (no-capture function pointer).125- `a.foo(b)` is sugar for `foo(a, b)` only when `a` is a struct/class value; primitives and126 lambda-typedef values need `|>`.127128## Collections129130```das131var a <- [1, 2, 3] // array<int> literal (heap)132let f = fixed_array(1, 2, 3) // fixed-size int[3] (stack)133var t <- { "hp" => 100, "mp" => 50 } // table<string; int> literal134var s : table<string> // one type param = a SET135let v = t?["hp"] ?? 0 // safe lookup with default136var sq <- [for (x in range(10)); x * x] // array comprehension137var m <- { for (x in range(5)); x => x * x } // table comprehension138```139140- `table[key]` on a mutable table INSERTS a default entry when missing - read with141 `t?[key] ?? default`, test with `key_exists(t, k)`.142- Never do two `[]` lookups on the same table in one expression - rehashing can invalidate143 the first reference.144- `push` copies, `emplace` moves (zeroes the source), `push_clone` deep-clones. Bulk forms145 `push_from` / `push_clone_from` take a whole array at once.146147## Move, copy, clone148149| Operator | Effect |150|---|---|151| `=` | copy - value types (`int`, `float`, `bool`, `string`, pointers, POD structs) |152| `<-` | move - transfers ownership, source is zeroed |153| `:=` | clone - deep copy, source unchanged |154155```das156def make() : array<int> {157 var r <- [1, 2, 3]158 return <- r // moving return for non-copyable types159}160```161162Structs are copyable only if all fields are. `delete` on a lambda requires `unsafe` - copies163alias one shared capture frame.164165## Pointers166167```das168var p = new Foo(x = 1) // heap allocation, type Foo?169let x = p?.x ?? 0 // safe navigation + null coalescing170var inscope q = new Foo() // auto-deleted at scope exit171unsafe {172 delete p // manual delete is unsafe173}174```175176- `T?` is a nullable pointer with TWO const positions: `Foo const?` = const pointee,177 `Foo? const` = const pointer. Writing through a parameter needs both non-const:178 `var p : Foo?`.179- `addr(x)` requires `unsafe`; `safe_addr(x)` yields a temporary pointer (`T?#`) without it.180181## Enums, variants, tuples, bitfields182183```das184enum Color {185 red186 green187}188let i = int(Color.red) // dot access; explicit cast to int189190var t = (1, 2.0, "three")191let first = t._0 // access by index192let (x, y, z) = t // destructuring193194bitfield Flags {195 read196 write197}198var fl : Flags = Flags.read | Flags.write199fl.write = false // single-bit assignment200if (fl.read) { ... } // single-bit test201```202203Without the explicit `: Flags`, `var fl = Flags.read | Flags.write` infers an ANONYMOUS204bitfield type and the named single-bit access (`fl.read`) stops compiling.205206Variants (tagged unions) are declared like structs, constructed with exactly one field207(`Value(asInt = 42)`), tested with `is`, read with `as` / `?as ... ?? default`. Writing a208variant field or switching the active field requires `unsafe`.209210## Memory model211212Locals and structs live on the stack; `new`, arrays and tables use the current context's213heap. Cleanup is deterministic and explicit - `delete`, `var inscope`, or moving ownership214out with `<-`; a plain local `var arr : array<int>` is NOT finalized at scope exit. Each215context (thread) owns its heap and cannot retain pointers into another's - clone what216crosses. `delete` on a container of pointers finalizes AND FREES every pointee; `clear()`217first when the pointers are non-owning.218219## Error handling220221- `panic("msg")`, and any runtime error (a failed bounds check, an error raised by a bound C++222 function), unwinds to the nearest enclosing `try { } recover { }`; the `recover` block runs223 and execution continues after it, in every execution tier. `recover` binds nothing - there is224 no exception value to match on; the message is `this_context().last_exception`225 (`require daslib/rtti`), with a trailing newline. Under the JIT the pair compiles to a226 builtin call, and a `return` inside the `try` block is a compile error there (the227 interpreter allows it). With no enclosing `try` the error unwinds out of the script: the228 daslang CLI prints it and exits nonzero, an embedding host gets it back as a context error.229 Never design APIs around panic-as-control-flow.230- `assert(cond, "msg")` may be stripped in release; `verify(cond, "msg")` always runs. The231 message must be a string CONSTANT - for a runtime-value diagnostic write232 `if (!cond) panic("bad n = {n}")`. An asserted expression with side effects (invoking a233 lambda counts) is rejected; use `verify`.234- Recoverable errors are values: return `bool`, `T?`, or a variant with an error case.235- `{ } finally { }` is SKIPPED on panic, by design - no must-run-on-failure cleanup there.236237## Callable types238239| Kind | Prefix | Allocation | Captures | Storable | Copyable |240|---|---|---|---|---|---|241| block | `$` | stack | surrounding scope by reference | no | no |242| lambda | `@` | heap | explicit modes: copy/move/clone/ref | yes | yes - pointer copy (alias) |243| function | `@@` | none | none | yes | yes |244245`@@name` is a pointer to a named function.246247## Gotchas248249- **No implicit conversions between values.** `float_var + int_var` is a compile error;250 cast one side. Bare integer LITERALS are the one exception, adapting to a known numeric251 target - `f + 1`, `d > 1`, `var f : float = 1`, `return 200` into `uint8` - but NOT at252 call arguments (`take_f(1)` fails when `take_f` wants float) or parameter defaults, and253 float literals never adapt to double. No `bool(int)` - write `x != 0`. No `string(bool)`254 - interpolate `"{flag}"`.255- **Hex literals are `uint`** - `int(0x3F)` when an int is needed.256- **Reserved words that look like identifiers:** `where`, `shared`, `label`, `expect`,257 `pass`, `explicit`, `capture`, `deref`, `template` are keywords; `range`, `urange`,258 `range64`, `urange64`, `block`, `function`, `lambda`, `iterator`, and the small-vector type259 names (`half2`, `short4`, `byte16`, ...) are type tokens. Using any as a260 variable/parameter/field/function or annotation-argument name is a syntax error -261 including `@range = 5` on a field.262- **A statement-level expression must fit one line** unless wrapped in `(...)`: a263 continuation line starting with `+` or `-` parses as a separate unary statement and is264 silently optimized away.265- **`options stack = N` counts only in the MAIN module** - a required library cannot raise266 the program's stack.267- **`require` paths use `/`**, resolve module mounts or same-directory names, and cannot268 contain hyphens; a file elsewhere in the tree needs a relative path with the explicit269 `.das` extension: `require ../lib/util.das`.270- **`with (self) { x = 1 }` brings struct fields into scope**; `assume alias = long.chain`271 makes a textual alias (re-evaluated each use, no copy).272273---274Verified against daslang 0.6.4 (2026-08-08). Maintained in the daslang repository under275`skills/daslang/`; report errors there.