Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variables and Types

Bindings

Variables in Kāra are declared with let:

let x = 42;
let name = "Kāra";
let pi = 3.14159;
let active = true;

The compiler infers the type from the value. You can also annotate explicitly:

let x: i32 = 42;
let name: String = "Kāra";

Mutability

Bindings are immutable by default. To allow reassignment, use let mut:

let x = 5;
// x = 10;  // compile error: x is immutable

let mut y = 5;
y = 10;     // ok

This is a deliberate default. Most values don't need to change, and immutability helps the compiler reason about your code — for ownership, for parallelization, and for correctness.

Primitive types

Kāra has the numeric types you'd expect:

TypeDescription
i8, i16, i32, i64Signed integers
u8, u16, u32, u64Unsigned integers
f32, f64Floating-point numbers
booltrue or false
charA Unicode scalar value: 'a', '\n', '\u{1F600}'

Integer literals default to i64, floats to f64. If you annotate a different type, the compiler checks that the literal fits:

let small: u8 = 255;    // ok
// let overflow: u8 = 256;  // compile error: 256 doesn't fit in u8

Numeric literals

Numbers can use underscores for readability and different bases:

let million = 1_000_000;
let flags = 0b1010_0011;   // binary
let color = 0xFF_AA_00;    // hex
let permissions = 0o755;   // octal

Strings

Kāra has two string types:

  • String — an owned, heap-allocated UTF-8 string.
  • StringSlice — a borrowed view into a string (like Rust's &str).
let greeting = "Hello";              // String
let multiline = """
    This is a
    multi-line string.
""";

String interpolation

Prefix a string with f to embed expressions:

let name = "world";
let msg = f"Hello, {name}!";

let x = 10;
let y = 20;
println(f"{x} + {y} = {x + y}");  // "10 + 20 = 30"

Type conversions

Kāra does not do implicit type conversions (except narrowing integer literals to annotated types). Use as for numeric casts:

let x: i64 = 1000;
let y: i32 = x as i32;

Shadowing

You can re-declare a binding with the same name. The new binding shadows the old one:

let x = 5;
let x = x + 1;       // x is now 6
let x = x * 2;       // x is now 12

Shadowing lets you transform a value through a series of steps without mut. Each let creates a new binding — the old one is gone.

Naming identifiers

Kāra enforces identifier naming at the compiler level — it's a grammar rule, not a style guide. Every identifier belongs to one of three case classes:

  • Type class — PascalCase. Structs, enums, enum variants, traits, generic type parameters: String, UserAccount, IoError, T.
  • Value class — snake_case, or a leading _ for intentionally-unused bindings. Functions, parameters, fields, modules, and let bindings inside function bodies: read_to_string, user_count, _tmp.
  • Const class — ALL_UPPER with underscores. Module-level let and let mut bindings: MAX_RETRIES, TIMEOUT_MS.
struct UserAccount { ... }                // Type class
fn read_to_string(path: String) { ... }   // Value class
let count = 0;                            // Value class (function body)

fn ReadFile(), struct user_account, and let pi = 3.14 at module scope are all compile errors. One quirk worth knowing: multi-word types treat acronyms as words — HttpClient, not HTTPClient; IoError, not IOError. That keeps the classification unambiguous at a glance.

Note: _ on its own isn't a name — it's a wildcard used in patterns, let _ = expr, pipes, and with _ effects. Only leading _ (like _tmp) is a valid identifier you can read later.

The point of enforcing this is that every Kāra codebase reads the same way — no per-project casing debates, no PR bikeshedding, no re-tuning when you move between libraries.

Module-level bindings

You can declare bindings at the top of a file, outside any function:

let MAX_RETRIES: i32 = 5;
let TIMEOUT_MS: i64 = 60 * 1000;
let APP_NAME: String = "myapp";

Module-level bindings must be initialized with compile-time constant expressions — no function calls, no I/O, no allocations. This is a deliberate restriction: there's no hidden code running before main, no initialization order bugs, no startup effects you can't see.

Values that need runtime initialization (config files, database connections) are constructed inside main and passed down. We'll cover this pattern in later chapters.