Beginner

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 integer
  • i64 — signed 64-bit integer
  • float — 64-bit floating point
  • booltrue or false
  • str — UTF-8 string
  • void — 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Pull the image
docker pull codearchaeology/vale:0.2

# Compile and run the basic variables example
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 \
  bash -c '/opt/vale/valec build mymod=variables.vale --output_dir build && ./build/main'

# Compile and run the type-annotated example
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 \
  bash -c '/opt/vale/valec build mymod=types_annotated.vale --output_dir build && ./build/main'

# Compile and run the mutability example
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 \
  bash -c '/opt/vale/valec build mymod=mutability.vale --output_dir build && ./build/main'

# Compile and run the conversions example
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 \
  bash -c '/opt/vale/valec build mymod=conversions.vale --output_dir build && ./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 use set to 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, and float requires explicit calls like toFloat and toInt, which prevents accidental precision loss.
  • Primitive types are simple and fewint, i64, float, bool, and str cover 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 for println or 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
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining