Variables and Types in Vale
Learn how variables, primitive types, immutability, and type inference work in Vale, an experimental systems language with generational references
Vale is a statically typed systems language with full type inference, single ownership, and generational references for memory safety. That combination shapes how variables work: by default, every local binding is final (immutable), the compiler infers most types, and reassigning a variable requires opting in to mutability with explicit syntax. This tutorial walks through how to declare variables, the primitive types Vale ships with, how mutability and type annotations are expressed, and how conversions between types are performed.
Because Vale aims to sit between high-level languages like Java and low-level languages like Rust, its type system feels familiar but with a few twists. There are no hidden allocations for primitive types, declarations don’t require let or var keywords, and mutability is a property of the binding (not the type). If you have written Rust, Scala, or Kotlin before, much of this will feel recognizable; if you are coming from C or Python, the immutable-by-default rule is the most important thing to internalize.
By the end of this tutorial you will be able to declare locals, apply type annotations when needed, distinguish final bindings from mutable ones, and convert between Vale’s basic types.
Declaring Variables
Vale locals are introduced by writing name = value;. There is no let, var, auto, or type keyword required — the compiler infers the type from the initializer. Bindings are final by default, meaning once assigned they cannot be reassigned.
Create a file named variables.vale:
import stdlib.*;
exported func main() {
// Type-inferred final bindings — the compiler picks the type from the literal
count = 42;
pi = 3.14159;
greeting = "Hello, Vale!";
is_ready = true;
println(count);
println(pi);
println(greeting);
println(is_ready);
}
The names on the left are not declarations of a “variable slot” in the C sense; they are bindings to values. Because the binding is final, the compiler can reason about ownership and lifetimes more easily, and you avoid an entire category of accidental-reassignment bugs.
Primitive Types and Type Annotations
Vale ships with a small set of built-in primitive types:
int— signed 32-bit integeri64— signed 64-bit integerfloat— 64-bit floating pointbool—trueorfalsestr— UTF-8 stringvoid— the empty type, the implicit return of functions that produce no value
When you want to be explicit (for documentation, or to force a particular numeric width), you can annotate the binding with the type after the name.
Create a file named types_annotated.vale:
import stdlib.*;
exported func main() {
// Explicit type annotations
age int = 30;
big i64 = 9000000000;
ratio float = 0.5;
active bool = false;
name str = "Vale";
println("age = " + str(age));
println("big = " + str(big));
println("ratio = " + str(ratio));
println("name = " + name);
println("active= " + str(active));
}
Type annotations use the form name type = value;. They are only required when the compiler cannot infer the type, when you want a non-default numeric width (such as i64 instead of int), or when you want the type to act as documentation at the declaration site.
Mutable Bindings
Final bindings cannot be reassigned. To opt in to mutation, append ! to the name when you declare the binding, and use the set keyword to assign a new value.
Create a file named mutability.vale:
import stdlib.*;
exported func main() {
// Final binding — cannot be reassigned
hours = 24;
// Mutable binding — note the ! after the name
counter! = 0;
set counter = counter + 1;
set counter = counter + 1;
set counter = counter + 1;
println("hours = " + str(hours));
println("counter = " + str(counter));
}
Two things worth noting here: mutability is a property of the binding (the !), not of the type itself, and assignment after the initial declaration always requires the set keyword. Forgetting either is a compile-time error, which makes it very obvious whether a piece of code is reading or mutating state.
Type Conversions
Vale does not perform implicit conversions between numeric types — even widening conversions like int to i64 must be written explicitly. Conversion functions are named after the target type and live in the standard library.
Create a file named conversions.vale:
import stdlib.*;
exported func main() {
// int -> float
whole = 7;
fractional = whole.toFloat();
// float -> int (truncates toward zero)
measured = 3.9;
truncated = measured.toInt();
// values -> str via the str() function
label = "value=" + str(whole);
println("whole = " + str(whole));
println("fractional = " + str(fractional));
println("truncated = " + str(truncated));
println(label);
}
The str(...) call on integers, floats, and booleans is the standard way to produce a printable representation. Conversion methods like toFloat and toInt make it explicit at the call site whenever a numeric width or representation changes, so there are no surprising precision losses hidden behind operator overloading.
Running with Docker
The same Docker image used for Hello World compiles and runs every example in this tutorial. The Vale compiler treats each .vale file as a module, so we pass the file via mymod= and run the resulting binary in build/main.
| |
The compiler prints progress for the frontend, backend, and linker stages. The lines after the final stage are your program’s output.
Expected Output
For variables.vale:
42
3.14159
Hello, Vale!
true
For types_annotated.vale:
age = 30
big = 9000000000
ratio = 0.5
name = Vale
active= false
For mutability.vale:
hours = 24
counter = 3
For conversions.vale:
whole = 7
fractional = 7
truncated = 3
value=7
Key Concepts
- Final by default — Bindings declared with
name = value;cannot be reassigned, encouraging code where state changes are rare and visible. - Mutability is opt-in — Add
!to the name at declaration and usesetto reassign; both pieces are required, so mutation always shows up at the read site. - Type inference everywhere — The compiler infers types from initializers, so type annotations are optional except for non-default widths or documentation.
- No implicit numeric conversions — Going between
int,i64, andfloatrequires explicit calls liketoFloatandtoInt, which prevents accidental precision loss. - Primitive types are simple and few —
int,i64,float,bool, andstrcover the basics; everything else is built up from structs and the standard library. str(...)produces printable text — Use it to convert any primitive to a string forprintlnor string concatenation.- Annotations follow the name — The pattern is
name type = value;, mirroring how field declarations look inside Vale structs.
Running Today
All examples can be run using Docker:
docker pull codearchaeology/vale:0.2
Comments
Loading comments...
Leave a Comment