Basics
This covers variables, types, expressions, and comments in Uplift.
Variables
let and var are local-only — they cannot appear at module scope. Use const { … } for module-level constants and global { … } for module-level mutable state.
Immutable Variables (let)
Use let to declare immutable local variables:
let x: i32 = 42 let name: string = "Alice" let active: bool = true
let x: i32 = 10 x = 20 // Error: cannot assign to immutable variable
Mutable Variables (var)
var counter: i32 = 0 counter = counter + 1 // OK counter = 10 // OK
Type Annotations
Type annotations are required when the type cannot be determined from the expression itself. Expressions whose type is obvious from syntax — literals, struct literals, enum literals, lambda expressions, type casts, and interpolated strings — do not need an annotation. All other expressions (identifiers, function calls, method calls, arithmetic, field access, indexing) require an explicit type annotation on the declaration.
// Self-documenting — annotation optional: let x = 42 // literal → i32 let s = "hello" // literal → string let flag = true // literal → bool let p = Point{x=1, y=2} // struct literal → Point let c = Color.Red // enum literal → Color var count = 0 // literal → i32
// Not self-documenting — annotation required: let a: i32 = x // identifier let b: i32 = a + 1 // binary expression let c: string = getName() // function call let d: i32 = p.x // field access let e: bool = items[0] // indexing let f: Point = p.translate(1, 2) // method call
Annotations are also needed to override the default type of a literal (e.g., let x: i64 = 42 defaults 42 to i32 without annotation).
Copy and Move
Every value supports .copy() and .move() as explicit operations:
.copy()— returns a copy of the value. The source stays alive. Available on all Copy types (primitives, strings) and structs that derive Copy..move()— transfers ownership. The source is consumed. Works on every type.
let a = 42 let b = a.copy() // 42, a still alive let c = a.move() // 42, a still alive (Copy types can't be consumed) let s = StrOwned.from("hi") let t = s.copy() // deep copy, s still alive let u = s.move() // ownership moves, s consumed
For non-Copy types (custom structs), .copy() requires the struct to derive Copy. .move() always works.
Module-Level Declarations
Constants and mutable globals at module scope are declared inside blocks:
const { … }
Compile-time constant values shared across the module. Single const declarations outside a block are not allowed.
const { MAX_SIZE: i32 = 1024, PI: f64 = 3.14159, NAME = "uplift", // type inferred }
Items inside can optionally carry meta annotations:
const { meta tag("config") MAX_ITEMS: i32 = 100, }
global { … }
Mutable module-level state. Initialized once at runtime, accessible from any function.
global { frame_count: i32 = 0, debug_mode: bool = false, timeout = 5000, // type inferred as i32 }
Trailing commas between items are optional in both block types.
Global Arrays
Static fixed-size arrays ([T; N]) can be declared in global blocks. Use @repeat to initialise every element to the same value.
global { buf: [i32; 256] = @repeat(0, 256), xs: [f64; 8] = @repeat(0.0, 8), flags: [bool; 16] = @repeat(false, 16), }
These are true LLVM globals — allocated in the data segment, accessible with runtime indices:
fn lookup(index: i32): i32 { let idx: usize = index.as @[usize] return buf[idx] // runtime index, bounds-checked }
Only fixed-size arrays are supported as globals. Slices ([T]) and Lists are heap-allocated and cannot be declared at module level.
Primitive Types
See Primitives for details.
Type Aliases
The type keyword creates a shorthand for an existing type:
type UserId = i32 type Vector = [f64; 3]
Type aliases are resolved at compile time and do not create new types — UserId and i32 are interchangeable.
The type keyword is also used in several other contexts:
All uses of type
The type keyword serves four distinct purposes in Uplift:
| Context | Syntax | Purpose |
|---|---|---|
| Top-level alias | type Name = Type |
Create a shorthand for an existing type |
| Associated type (trait) | type Name |
Declare a type that trait implementors must provide |
| Associated type binding (impl) | type Name = Type |
Provide the concrete type when implementing a trait |
| Extern opaque type | extern type Name |
Declare a C type with unknown layout |
Associated Types in Traits and Impls
The type keyword inside traits and impls works much like a function declaration — type is the keyword, followed by the name:
trait Collection { type Element fn get_first(self: Self): Self.Element }
When implementing the trait, bind the associated type to a concrete type:
impl IntArray: Collection { type Element = i32 fn get_first(self: IntArray): i32 { return self.data[0] } }
Literals
Integer Literals
let decimal: i32 = 42 let hex: i32 = 0xFF let binary: i32 = 0b1010 let octal: i32 = 0o77 let with_underscore: i64 = 1_000_000
Float Literals
Float literals require a decimal point (.) or an exponent (e). Without a decimal point, a number is an integer even if it looks like a float value.
let pi: f64 = 3.14159 let scientific: f64 = 1.5e10 let negative_exp: f64 = 2.5e-3
When type inference is used, the presence of a . determines the inferred type:
let x = 3.14 // inferred as f64 (has decimal point) let y = 42 // inferred as i32 (no decimal point) let z = 0.0 // inferred as f64 (decimal point present) let w = 0 // inferred as i32 (no decimal point)
Boolean Literals
let yes: bool = true let no: bool = false
Character Literals
let letter: char = 'a' let newline: char = '\n' let tab: char = '\t' let quote: char = '\'' let backslash: char = '\\'
String Literals
let greeting: string = "Hello, World!" let with_escapes: string = "Line 1\nLine 2" let with_quotes: string = "She said \"Hello\""
String Interpolation
let name: string = "Alice" let age: i32 = 30 let message: string = "Name: ${name}, Age: ${age}" // Field access works too struct Point { x: i32, y: i32 } let p: Point = Point { x: 10, y: 20 } println("Point: (${p.x}, ${p.y})")
Expressions
Arithmetic Operators
let a: i32 = 10 let b: i32 = 3 let sum: i32 = a + b // 13 let diff: i32 = a - b // 7 let prod: i32 = a * b // 30 let quot: i32 = a / b // 3 (integer division) let rem: i32 = a % b // 1 (remainder)
Overflow Operators
let x: i32 = 2147483647 // Max i32 // These would panic at runtime (default): // let overflow: i32 = x + 1 // Wrapping arithmetic (wraps around) let wrapped: i32 = x +% 1 // -2147483648 // Saturating arithmetic (clamps at max/min) let saturated: i32 = x +| 1 // 2147483647
Comparison Operators
let a: i32 = 10 let b: i32 = 20 let eq: bool = a == b // false let ne: bool = a != b // true let lt: bool = a < b // true let le: bool = a <= b // true let gt: bool = a > b // false let ge: bool = a >= b // false
Logical Operators
let a: bool = true let b: bool = false let and_result: bool = a and b // false let or_result: bool = a or b // true let not_result: bool = not a // false
Bitwise Operators
let a: i32 = 0b1100 let b: i32 = 0b1010 let and_bits: i32 = a & b // 0b1000 let or_bits: i32 = a | b // 0b1110 let xor_bits: i32 = a ^ b // 0b0110 let not_bits: i32 = ~a // Bitwise NOT let left: i32 = a << 2 // 0b110000 let right: i32 = a >> 2 // 0b0011
Comments
Single-Line Comments
// This is a single-line comment let x: i32 = 42 // Inline comment
Multi-Line Comments
/* This is a multi-line comment */ /* Comments can /* nest */ like this */
Blocks
fn main(): i32 { let x: i32 = 10 { let y: i32 = 20 // y is only visible in this block println("${y}") } // y is not accessible here return x }
Next Steps
- Functions - Function definitions
- Control Flow - if, match, loops
- Primitives - Detailed type information