Types and bindings
In Python or JavaScript, x = 7 just works: the language figures out at runtime that x is a number. Glide is different. Every value has a type the compiler knows about before the program runs, and you usually write the type next to the name. The reward for being explicit is that the compiler can catch a huge class of bugs — passing a string where you wanted a number, reading a field that doesn't exist — before you ship.
Bindings: let, mut, const
The simplest thing you can do is bind a value to a name:
let x: i32 = 42;
Read that as "let there be a variable named x of type i32 with the value 42." i32 is short for "32-bit signed integer" — more on widths in a moment.
By default let bindings are immutable: once you've written the value, it can't change.
let x: i32 = 42;
x = 7; // ✗ error: `x` is not mutable
If you actually need to change the value later, add mut:
let mut count: i32 = 0;
count = count + 1; // ✓ count is mutable
count = count + 1;
const is for values fixed at compile time — known before the program even starts running:
const MAX_RETRIES: i32 = 5;
const GREETING: string = "hello";
The compiler bakes these into the binary. They can't depend on user input, file contents, or anything else that's only known at runtime.
Type inference
You can leave the type off when the right-hand side makes it obvious:
let n = 42; // inferred as i32
let pi = 3.14; // inferred as f64
let name = "glide"; // inferred as string
But on function signatures (parameters and return type) the types are mandatory. The reasoning: signatures are documentation; surprises don't help. You'll see this in the next chapter.
Integers, spelled by width
Glide doesn't ship a generic int. You pick a width that matches what the value will hold:
let a: i8 = -100; // -128 .. 127
let b: i16 = 30_000; // ±32K
let c: i32 = 2_000_000_000; // ±2 billion — the usual default
let d: i64 = 9_000_000_000; // ±9 quintillion
let e: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;
let p: u8 = 255; // unsigned: 0 .. 255 (no negatives)
let q: u16 = 65535;
let r: u32 = 4_000_000_000;
let s: u64 = 18_000_000_000_000_000_000;
The i means signed (can be negative), the u means unsigned (only zero and up). The number is the bit-width — a higher number = bigger range = more memory used per value.
Underscores in numeric literals are pure visual help — 1_000_000 is the same as 1000000. Literals can be decimal, hex, or binary:
let mask: u32 = 0xFFFF_0000; // hex
let bits: u8 = 0b1010_1100; // binary
let count: i32 = 1_000_000; // plain decimal
There are no implicit narrowing conversions. To go from i64 to i32 you write the cast yourself with as:
let big: i64 = 100;
let small: i32 = big as i32;
Floats
Two widths: f32 (about 7 decimal digits of precision) and f64 (about 15). f64 is the default for unannotated decimal literals — it's what you almost always want. (float is an alias you'll see in some code; it's f64.)
let g: f32 = 1.5;
let h: f64 = 2.718281828;
Glide does not auto-convert integers to floats. let x: f64 = 1; is an error — write 1.0 or 1 as f64.
Booleans
bool holds true or false. It's a real type, distinct from integers — if 1 { ... } is rejected by the compiler.
let ready: bool = true;
let done: bool = false;
if ready && !done {
println!("go");
}
The operators are && (and), || (or), ! (not).
Strings
string is a UTF-8 byte sequence — the text type. Double-quoted literals with the usual escapes (\n, \t, \", \\):
let s: string = "hello, glide";
println!(s);
println!("length:", s.len());
println!("upper:", s.to_upper());
println!("first 5:", s.substring(0, 5));
Inside a literal, ${expr} interpolates an expression — same pattern as JavaScript template literals:
let n: i32 = 7;
let msg: string = "got ${n} results";
A few string methods you'll use a lot: .len(), .eq(other), .contains(sub), .starts_with(p), .ends_with(s), .trim(), .to_upper(), .to_lower(), .split(sep), .replace(find, repl), .substring(start, end), .index_of(sub), .concat(other).
Pointers
This is where Glide starts to diverge from Python visibly. A pointer is a value that holds the address of another value. The type *T means "pointer to a T."
let x: i32 = 42;
let p: *i32 = &x; // & means "take the address of"
println!(*p); // * means "follow the pointer" → 42
You won't write &x and *p a lot for scalars. Where pointers show up everywhere is for heap-allocated objects — values that live on the heap because they grow at runtime:
let v: *Vector<i32> = Vector::new();
v.push(1);
v.push(2);
Here Vector::new() allocates a fresh vector on the heap and gives you back a pointer to it. You can call methods on the pointer directly (v.push(...)); Glide figures out whether to dereference automatically.
Memory model is the whole chapter dedicated to how this works without needing a garbage collector. For now: any value created with Type::new() lives on the heap, and Glide tracks who owns it and inserts the right cleanup automatically.
Structs
A struct is a fixed set of named fields packaged together. You define the type once and build values with the struct-literal syntax:
struct Point {
x: f64,
y: f64,
}
fn main() -> i32 {
let origin: Point = Point { x: 0.0, y: 0.0 };
let p: Point = Point { x: 3.0, y: 4.0 };
let dist: f64 = (p.x * p.x + p.y * p.y);
println!("distance² from origin:", dist);
return 0;
}
Read fields with .field. If you've used Python dataclasses, structs feel similar — except every field has a type, and you can't add a field that wasn't in the declaration.
Vectors
Vector<T> is a growable list of values, all of type T. Like a Python list, but every element has the same type. It lives on the heap and owns its contents:
let v: *Vector<i32> = Vector::new();
v.push(1);
v.push(2);
v.push(3);
println!("len:", v.len());
println!("first:", v.get(0));
for x in v {
println!(x);
}
The push_all! macro pushes several values in one line — handy for seeding a vector:
let primes: *Vector<i32> = Vector::new();
primes.push_all!(2, 3, 5, 7, 11);
HashMaps
HashMap<V> is a string-keyed hash table. The values can be any type V:
let h: *HashMap<i32> = HashMap::new();
h.insert("alice", 30);
h.insert("bob", 25);
if h.contains("alice") {
println!("alice is", h.get("alice"));
}
For non-string keys you reach for the typed variants in stdlib::hashmap — we'll come back to that when we need it.
A first look at ?T and !T
Two more types you'll see everywhere in Glide code, covered in depth in Errors as values:
- `?T` — "maybe a T". Either
some(value)ornone(). Use it when a function might not have anything to return. - `!T` — "a T or an error message". Either
ok(value)orerr("message"). Use it when an operation can fail.
fn first_byte(s: string) -> ?i32 {
if s.len() == 0 { return none(); }
return some(s.at(0).to_int());
}
fn parse(s: string) -> !i32 {
if s.eq("") { return err("empty input"); }
return ok(42);
}
Where to next
Values are catalogued. Next you'll move them around with Functions and control flow.