Data Types

Every value in Oxide has a data type, which tells the compiler what kind of data is being specified so it knows how to work with that data. Oxide is statically typed, meaning the compiler must know the types of all variables at compile time. The compiler can usually infer types from values and how we use them, but when many types are possible, we must add a type annotation.

Scalar Types

A scalar type represents a single value. Oxide has four primary scalar types: integers, floating-point numbers, Booleans, and characters. Oxide provides intuitive type aliases that will feel familiar if you're coming from Swift, Kotlin, or TypeScript.

Integer Types

An integer is a number without a fractional component. Oxide provides signed and unsigned integers of various sizes:

Oxide TypeRust EquivalentSizeRange
Int8i88-bit-128 to 127
Int16i1616-bit-32,768 to 32,767
Int32i3232-bit-2.1B to 2.1B
Int64i6464-bitVery large
Inti3232-bitDefault signed integer
IntSizeisizearchPointer-sized signed
UInt8u88-bit0 to 255
UInt16u1616-bit0 to 65,535
UInt32u3232-bit0 to 4.3B
UInt64u6464-bitVery large
UIntu3232-bitDefault unsigned integer
UIntSizeusizearchPointer-sized unsigned

The Int type (which maps to Rust's i32) is the default choice for integers and is generally the fastest, even on 64-bit systems. Use UIntSize when indexing collections, as it matches the size of memory addresses on your system.

fn main() {
    let age: Int = 30
    let temperature: Int = -15
    let count: UIntSize = 1000

    // Type inference works too
    let inferred = 42  // Defaults to Int
}

Integer Literals

You can write integer literals in various forms:

LiteralExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000

Note that underscores can be inserted for readability: 1_000_000 is the same as 1000000.

Floating-Point Types

Oxide has two floating-point types for numbers with decimal points:

Oxide TypeRust EquivalentSizePrecision
Float32f3232-bit~6-7 digits
Float64f6464-bit~15-16 digits
Floatf6464-bitDefault floating-point

The Float type (which maps to Rust's f64) is the default because modern CPUs handle double-precision floats nearly as fast as single-precision, and it provides more accuracy.

fn main() {
    let pi: Float = 3.14159
    let temperature = 98.6  // Inferred as Float
    let precise: Float64 = 2.718281828459045

    // Scientific notation
    let avogadro: Float = 6.022e23
}

Numeric Operations

Oxide supports the standard mathematical operations: addition, subtraction, multiplication, division, and remainder:

fn main() {
    // Addition
    let sum = 5 + 10

    // Subtraction
    let difference = 95.5 - 4.3

    // Multiplication
    let product = 4 * 30

    // Division
    let quotient = 56.7 / 32.2
    let truncated = 5 / 3  // Results in 1 (integer division)

    // Remainder
    let remainder = 43 % 5
}

The Boolean Type

Oxide's Boolean type has two possible values: true and false. Booleans are one byte in size and are specified using the Bool type:

fn main() {
    let isActive: Bool = true
    let isComplete = false  // Inferred as Bool

    // Booleans are often the result of comparisons
    let isGreater = 5 > 3  // true
}

Rust comparison: Oxide uses Bool instead of bool, following the convention of capitalizing type names.

The Character Type

The char type represents a single Unicode scalar value. Character literals use single quotes:

fn main() {
    let letter = 'a'
    let emoji = '😀'
    let heart = '❤'
}

The char type is four bytes and represents a Unicode Scalar Value, which means it can represent much more than just ASCII.

Compound Types

Compound types can group multiple values into one type. Oxide has two primitive compound types: tuples and arrays.

Tuples

A tuple groups together values of different types into one compound type. Tuples have a fixed length; once declared, they cannot grow or shrink.

fn main() {
    let tup: (Int, Float, Bool) = (500, 6.4, true)

    // Destructuring
    let (x, y, z) = tup
    println!("The value of y is: \(y)")

    // Access by index
    let five_hundred = tup.0
    let six_point_four = tup.1
    let is_true = tup.2
}

The tuple without any values, (), is called the unit type and represents an empty value or empty return type.

Arrays

Arrays contain multiple values of the same type with a fixed length. Use square brackets for array literals:

fn main() {
    let numbers: [Int; 5] = [1, 2, 3, 4, 5]
    let months: [&str; 12] = [
        "January", "February", "March", "April",
        "May", "June", "July", "August",
        "September", "October", "November", "December"
    ]

    // Initialize with same value
    let zeros: [Int; 5] = [0; 5]  // [0, 0, 0, 0, 0]

    // Accessing elements
    let first = numbers[0]
    let second = numbers[1]
}

Arrays are useful when you want data on the stack rather than the heap, or when you need a fixed number of elements. For a collection that can grow or shrink, use Vec<T> instead.

The String Type

Oxide has two string types:

  • str: A string slice, usually seen as &str. This is an immutable reference to string data.
  • String: A growable, heap-allocated string.
fn main() {
    // String literal (type is &str)
    let greeting = "Hello, world!"

    // Create an owned String
    let name: String = "Alice".toString()

    // String with interpolation
    let message = "Hello, \(name)!"
    println!("\(message)")
}

String interpolation with \(expression) is a key Oxide feature. Any expression inside \() is evaluated and converted to a string:

fn main() {
    let count = 42
    let price = 19.99

    println!("Count: \(count), Price: $\(price)")
    println!("Total: $\(count as Float * price)")
}

Rust comparison: Rust uses format!("{}", x) for string formatting. Oxide's \(x) syntax is inspired by Swift and is more concise.

#![allow(unused)]
fn main() {
// Rust
println!("Count: {}, Price: ${}", count, price);
}

Nullable Types

Oxide has first-class support for nullable (optional) types using the ? suffix. A T? type can hold either a value of type T or null:

fn main() {
    let maybeNumber: Int? = 42
    let nothing: String? = null

    // Check if value exists
    if let number = maybeNumber {
        println!("Got number: \(number)")
    }

    // Provide a default with ??
    let value = maybeNumber ?? 0

    // Force unwrap with !! (use carefully!)
    let forced = maybeNumber!!
}

The T? syntax is equivalent to Rust's Option<T>, and null is equivalent to None:

OxideRust
Int?Option<i32>
String?Option<String>
nullNone
Some(x)Some(x)
x ?? yx.unwrap_or(y)
x!!x.unwrap()
fn findUser(id: Int): User? {
    if id == 1 {
        Some(User { name: "Alice".toString() })
    } else {
        null
    }
}

fn main() {
    let user = findUser(1) ?? User { name: "Guest".toString() }
    println!("Hello, \(user.name)")
}

Collection Types

Oxide uses Rust's standard collection types directly:

Vectors

Vec<T> is a growable array type:

fn main() {
    // Create a vector with the vec! macro
    var numbers: Vec<Int> = vec![1, 2, 3]

    // Add elements
    numbers.push(4)
    numbers.push(5)

    // Access elements
    let first = numbers[0]
    let maybe_tenth: Int? = numbers.get(10).copied()

    // Iterate
    for num in numbers.iter() {
        println!("\(num)")
    }
}

HashMaps

HashMap<K, V> stores key-value pairs:

import std.collections.HashMap

fn main() {
    var scores: HashMap<String, Int> = HashMap.new()

    scores.insert("Blue".toString(), 10)
    scores.insert("Red".toString(), 50)

    let blue_score = scores.get(&"Blue".toString())

    for (team, score) in scores.iter() {
        println!("\(team): \(score)")
    }
}

Note: Oxide v1.0 uses Rust's collection names directly (Vec, HashMap) rather than providing aliases like Array or Dict. This helps you learn the actual Rust types you'll encounter in the ecosystem.

Type Inference

Oxide has strong type inference. The compiler can usually figure out types from context:

fn main() {
    let x = 5          // Int
    let y = 3.14       // Float
    let z = true       // Bool
    let s = "hello"    // &str

    var items = vec![] // Vec<???> - needs annotation or usage
    items.push(1)      // Now compiler knows it's Vec<Int>
}

When the compiler cannot infer the type, you need to provide an annotation:

fn main() {
    // Compiler needs help here
    let guess: Int = "42".parse().unwrap()

    // Or specify the type in the turbofish
    let guess = "42".parse<Int>().unwrap()
}

Summary

Oxide provides intuitive type names that feel familiar to developers from many language backgrounds while mapping directly to Rust's type system:

  • Integers: Int, Int64, UInt, UIntSize, etc.
  • Floats: Float, Float32, Float64
  • Boolean: Bool
  • Character: char
  • Tuples: (T, U, V)
  • Arrays: [T; N]
  • Strings: &str, String with \(expr) interpolation
  • Nullable: T? with null, ??, and !! operators
  • Collections: Vec<T>, HashMap<K, V>

Since Oxide types ARE Rust types (just with different names), you get full compatibility with the entire Rust ecosystem.