3.3. Values and Data Types
Daslang is a strong, statically typed language. All variables have a type. Daslang’s basic POD (plain old data) data types are:
int, uint, float, bool, double, int64, uint64
int2, int3, int4, uint2, uint3, uint4, float2, float3, float4,
range, urange, range64, urange64
All PODs are represented with machine register/word. All PODs are passed to functions by value.
Daslang’s storage types are:
int8, uint8, int16, uint16 - 8/16-bits signed and unsigned integers
They have no arithmetic of their own, but can be used as a storage type within structs, classes, etc.
Daslang’s 16/8-bit vector lattice extends the storage tier with packed small-element vectors:
float16 - IEEE-754 binary16 scalar (`half` is an alias)
half2, half3, half4, half8 - packed float16 vectors
short2, short3, short4, short8 - packed int16 vectors
ushort2, ushort3, ushort4, ushort8 - packed uint16 vectors
byte2, byte3, byte4, byte8, byte16 - packed int8 vectors (byte is SIGNED)
ubyte2, ubyte3, ubyte4, ubyte8, ubyte16 - packed uint8 vectors
Lattice values are tightly packed (half3 is 6 bytes, byte3 is 3 bytes; alignment equals
the element size) and pass by value like other PODs. The fp16 family has closed arithmetic:
a * b + c on half4 yields half4, computed by promoting each lane to float,
computing, and rounding back — correctly rounded per operation, bit-identical to native fp16
hardware. Division follows IEEE (1h / 0h is inf; NaN != NaN). float16 literals
use the h suffix: 1.5h. The integer families carry storage, converts, and the integer
bit surface — but no arithmetic (byte4 + byte4 is a compile error): widen with the wider
ctor of matching signedness (int4(b4) sign-extends, short8(b8); unsigned lanes widen
unsigned, uint4(us4) — there is no cross-signed form such as int4(us4)), narrow with
the ctor (C truncation, short4(i4)) or the saturating _sat forms (short4_sat(i4),
byte8_sat(s8), clamping to the target range). The bit surface matches the 32-bit vector families:
<< >> & | ^ and their compound assigns. Shifts take a scalar int count masked to the
lane width (b16 >> 9 equals b16 >> 1); signed lanes shift right arithmetically,
unsigned logically; << wraps each lane — shuffle(lut, nib >> 4) is the nibble-unpack
shape. The fp16 family converts to and from float per arity (half4(f4) rounds each lane,
float4(h4) is exact) plus half8_lo / half8_hi, which unpack a half8 into
float4 halves, and the two-argument compose half8(f4lo, f4hi), whose operands are both
float4 — there is no half8(half4, half4) form.
The other exception to storage-only is the 16-lane 8-bit forms, which carry exact integer
dot-product intrinsics — the hardware dot4 class (NEON sdot/usdot, AVX-VNNI
vpdpbusd): idot4(a, b) dots byte16 operands in groups of four lanes into int4,
idot4(acc, a, b) accumulates, and idot(a, b) reduces all 16 lanes to one int. The
first operand may instead be ubyte16 (lanes zero-extended, the quantized-kernel shape); the
result is always signed and exact — no lane sum can overflow int. shuffle(lut, idx)
selects bytes from a 16-entry table (lut[idx & 15] per lane, the tbl/pshufb
semantic); lut is a byte16 and the index operand must be a ubyte16 (a byte16
index does not match). For shader code, the packed single-group forms sdot4/usdot4
(two uint words as 4+4 8-bit lanes) live in daslib/shader_lingua_franca and lower to
native GPU ops.
Beyond xyzw (which stays capped at 4 lanes), all vectors support OpenCL-style .s
swizzles — s followed by one hex lane digit per output lane, repeats allowed: v.s0,
v.s3210, b16.sf, h8.s0246. The 8/16-lane forms add .lo / .hi for their
half-vectors. Sequential swizzles are writable (v.xy = half2(1h, 2h); v.s0 = 9h); the
two namespaces never mix in one mask.
Daslang’s other types are:
string, das_string, struct, pointers, references, block, lambda, function pointer,
array, table, tuple, variant, iterator, bitfield
All Daslang’s types are initialized with zeroed memory by default.
3.3.1. Integer
An integer represents a 32-bit (un)signed number:
let a = 123 // decimal, integer
let u = 123u // decimal, unsigned integer
let h = 0x0012 // hexadecimal, unsigned integer
let a = int2(123, 124) // two integers type
let u = uint2(123u, 124u) // two unsigned integer type
3.3.2. Float
A float represents a 32-bit floating point number:
let a = 1.0
let b = 0.234
let a = float2(1.0, 2.0)
3.3.3. Bool
A bool is a two-valued (Boolean) data type. Its literals are true
and false. A bool value expresses the validity of a condition
(tells whether the condition is true or false):
let a = true
let b = false
All conditionals (if, elif, while) work only with the bool type.
3.3.4. String
Strings are an immutable sequence of characters. In order to modify a string, it is necessary to create a new one.
Daslang’s strings are similar to strings in C or C++. They are
delimited by quotation marks(") and can contain escape
sequences (\t, \a, \b, \n, \r, \v, \f,
\\, \", \', \0, \x<hh>, \u<hhhh> and
\U<hhhhhhhh>):
let a = "I'm a string\n"
let a = "I'm also
a multi-line
string\n"
Strings type can be thought of as a ‘pointer to the actual string’, like a ‘const char *’ in C. As such, they will be passed to functions by value (but this value is just a reference to the immutable string in memory).
das_string is a mutable string, whose content can be changed. It is simply a builtin handled type, i.e., a std::string bound to Daslang.
As such, it passed as reference.
3.3.5. Type Conversion and Casting
Daslang has no implicit type conversions between values — non-literal int and
float operands always need explicit casts. For example, with two named variables,
int + float is a compilation error and you must convert explicitly:
let i = 42
let f = float(i) + 1.0 // explicit int -> float
let i2 = i + int(1.0) // explicit float -> int
Bare integer literals are a narrow exception to this rule — see Integer literal promotion below.
3.3.5.1. Explicit numeric casts
Any numeric type can be explicitly converted to any other numeric type using the target type name as a function:
float(42) // int -> float (42.0)
int(3.7) // float -> int, truncates (3)
double(3.14) // float -> double
float(3.14lf) // double -> float
uint(42) // int -> uint
int64(42) // int -> int64
uint64(42) // int -> uint64
int8(42) // int -> int8 (storage type)
uint8(42) // int -> uint8 (storage type)
int16(42) // int -> int16 (storage type)
uint16(42) // int -> uint16 (storage type)
Float-to-integer conversion truncates toward zero (like C).
3.3.5.2. Integer literal promotion
A bare integer literal (1, -13, 0xFF) is implicitly promoted to a
matching numeric target type when the literal’s value fits. This eliminates
boilerplate float(...) / uint8(...) casts on small constants without
opening the door to general implicit conversions.
3.3.5.2.1. Where promotion applies
Promotion is not a general “the compiler already knows the target type” rule. It fires in a fixed set of contexts, and nowhere else:
var a : float = 1 // local var init
var g : uint8 = 250 // global var init (module scope)
struct Foo { x : float = 7 } // struct field decl init
var f = Foo(x = 3) // struct ctor field init
var v = V(arm = 42) // variant arm init
var b : float ; b = 1 // copy
var c : float ; c := 1 // clone
var d : float ; d += 1 // compound assignment
var e : float = a + 1 // binary operator (either side)
def fn() : uint8 { return 200 } // return statement
3.3.5.2.2. Where promotion does not apply
Function-call arguments, function-parameter default values, and ExprMove
(<-) are intentionally not promoted, even though the target type is
plainly visible in each case:
def take_f(a : float) {}
def f(a : float = 1) {} // error[30161]: function argument default value
// type mismatch; 'float const' vs 'int const'
take_f(1) // error[30341]: no matching functions or generics:
// take_f(int const)
var m : float ; m <- 1 // error[30941]: can only move compatible type;
// float& -const = int const
Spell the literal in the target type instead: take_f(1.0f) (or
take_f(float(1))), and def f(a : float = 1.0f).
Only integer literals promote. A float literal never promotes to double:
var d : double = 1.0 // error[30344]: local variable d initialization
// type mismatch; double -const = float const
Spell the double literal instead:
var d : double = 1.0lf // ok — double literal
Accepted target types: int8 / int16 / int / int64,
uint8 / uint16 / uint / uint64,
bitfield8 / bitfield16 / bitfield / bitfield64,
float, double. Out of scope: enum, pointer, string, struct.
3.3.5.2.3. Range checks for integer/bitfield targets
For integer and bitfield targets, the literal is range-checked against the
target’s exact range. A value that doesn’t fit raises a single
error[30515] exceeds_constant_range and no downstream type-mismatch
error:
var too_big : uint8 = 256
// error[30515]: constant value 256 does not fit in uint8
// expected range [0..255]
var too_small : int8 = -129 // out of range
var negative : uint8 = -1 // negative literal, unsigned target
3.3.5.2.4. Float and double — precision is a lint warning
Promotion to float or double always succeeds at infer time, regardless
of value. float can exactly represent every int in [-2^24, 2^24];
above that range some ints round during the cast. The precision check is
deferred to daslib/lint as LINT011 so general code keeps compiling:
let exact : float = 16777216 // 2^24 — exact, no warning
let inexact : float = 16777217 // 2^24 + 1 — LINT011 fires
let suppr : float = 16777219 // nolint:LINT011
double can represent every int up to 2^53; since promotion sources
top out at uint32 (2^32 - 1), LINT011 never fires on double
targets — the check is wired symmetrically so future broader sources stay
covered.
3.3.5.3. Enumeration casts
Enumerations can be converted to their underlying integer type:
enum Color {
red
green
blue
}
let c = Color.green
let i = int(c) // 1
Converting an integer back to an enumeration requires unsafe and reinterpret:
unsafe {
let c2 = reinterpret<Color>(1) // Color.green
}
3.3.5.4. String conversion
Any type can be converted to a string via the string function:
let s = string(42) // "42"
let s2 = string(3.14) // "3.14"
String interpolation ({expr} inside string literals) also converts expressions to text automatically.
To parse strings into numbers, use the functions from require strings:
require strings
let i = to_int("123") // 123
let f = to_float("3.14") // 3.14
There is no int(string) — use to_int instead.
3.3.5.5. What is NOT allowed
No implicit value-to-value numeric conversion: with two named variables
i : intandf : float,i + fis a compile error — wrap one side withfloat(i)/int(f). Bare integer literals are the only exception (see Integer literal promotion above).No bool(int): use a comparison like
x != 0insteadNo int(string): use
to_intfrom thestringsmodule
3.3.6. Table
Tables are associative containers implemented as a set of key/value pairs:
var tab: table<string; int>
tab["10"] = 10
tab["20"] = 20
tab["some"] = 10
tab["some"] = 20 // replaces the value for 'some' key
(see Tables).
3.3.7. Array
Arrays are simple sequences of objects. There are static arrays (fixed size) and dynamic arrays (container, size is dynamic). The index always starts from 0:
var a = fixed_array(1, 2, 3, 4) // fixed size of array is 4, and content is [1, 2, 3, 4]
var b: array<string> // empty dynamic array
push(b,"some") // now it is 1 element of "some"
(see Arrays).
3.3.8. Struct
Structs are records of data of other types (including structs), similar to C. All structs (as well as other non-POD types, except strings) are passed by reference.
(see Structs).
3.3.9. Classes
Classes are similar to structures, but they additionally allow built-in methods and rtti.
(see Classes).
3.3.10. Variant
Variant is a special anonymous data type similar to a struct, however only one field exists at a time. It is possible to query or assign to a variant type, as well as the active field value.
(see Variants).
3.3.11. Tuple
Tuples are anonymous records of data of other types (including structs), similar to a C++ std::tuple. All tuples (as well as other non-POD types, except strings) are passed by reference.
(see Tuples).
3.3.12. Enumeration
An enumeration binds a specific integer value to a name, similar to C++ enum classes.
(see Enumerations).
3.3.13. Bitfield
Bitfields are an anonymous data type, similar to enumerations. Each field explicitly represents one bit, and the storage type is always a uint. Queries on individual bits are available on bitfields, as well as binary logical operations.
(see Bitfields).
3.3.14. Function
Functions are similar to those in most other languages:
def twice(a: int): int {
return a + a
}
However, there are generic (templated) functions, which will be ‘instantiated’ during function calls by type inference:
def twice(a) {
return a + a
}
let f = twice(1.0) // 2.0 float
let i = twice(1) // 2 int
(see Functions).
3.3.15. Reference
References are types that ‘reference’ (point to) some other data:
def twice(var a: int&) {
a = a + a
}
var a = 1
twice(a) // a value is now 2
All structs are always passed to functions arguments as references.
3.3.16. Pointers
Pointers are types that ‘reference’ (point to) some other data, but can be null (point to nothing) (see Pointers). In order to work with actual value, one need to dereference it using the dereference or safe navigation operators. Dereferencing will panic if a null pointer is passed to it. Pointers can be created using the new operator, or with the C++ environment.
def twice(var a: int&) {
a = a + a
}
def twicePointer(var a: int?) {
twice(*a)
}
struct Foo {
x: int
}
def getX(foo: Foo?) { // it returns either foo.x or -1, if foo is null
return foo?.x ?? -1
}
3.3.17. Smart Pointers
Smart pointers (smart_ptr<T>) are reference-counted pointers to C++-managed (handled) types.
They are not available for regular Daslang structs or classes — only for types registered
as handled types from the C++ side.
Note
Most AST node types (Expression, Function, Structure, Enumeration,
Variable, MakeFieldDecl, MakeStruct) are not smart pointers.
They are garbage-collected (gc_node) types — new returns a raw pointer (T?)
and the GC manages their lifetime. Use plain assignment (=) and plain return,
not var inscope, <-, or return <-.
Smart pointers are still used for a few non-GC types such as Context.
Smart pointers in AST code:
require daslib/ast
// GC types — plain assignment, no inscope, no <-
var expr = new ExprConstInt(value=42) // Expression is gc_node
var fn = new ExprCall(at=expr.at, name:="foo")
// Visitor adapter — use block-based make_visitor
make_visitor(*visitor) $ (adapter) { // adapter alive inside the block
visit(program, adapter)
}
The key properties of smart pointers:
They maintain a reference count and automatically release the object when the count reaches zero
They can be moved with
<-, but never copied with=Dereferencing works the same as regular pointers (
*ptrandptr.field)Move-assignment into a variable that is already live requires
unsafe(error[31029]); move-initialization of a freshvar inscopedoes not
Because strict_smart_pointers is enabled by default, smart pointer variables must be
declared with inscope to ensure automatic cleanup — a plain var raises
error[31018] requires var inscope to be safe:
require daslib/ast
var inscope a <- make_file_access("") // create — safe, no unsafe needed
var inscope b <- a // move-initialize — safe, a becomes null
unsafe {
b <- a // move-assign into a live variable — unsafe
}
3.3.17.1. Ownership transfer functions
Daslang provides built-in functions for safe smart pointer ownership transfer.
These avoid the need for unsafe blocks when reassigning smart pointers that
already hold a value:
move(dest, src)Transfers ownership from
srcintodest. Ifdestalready holds a value, its reference count is decremented. After the call,srcbecomes null.smart_ptr_clone(dest, src)Clones (increments the reference count of)
srcintodest. Bothdestandsrcremain valid after the call. Ifdestalready held a value, it is released.smart_ptr_use_count(ptr)Returns the current reference count of the smart pointer as a
uint.
3.3.18. Iterators
Iterators are a sequence which can be traversed, and associated data retrieved. They share some similarities with C++ iterators.
(see Iterators).