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:

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