Data Types
Now that you know how to create variables and constants, it’s time to learn what kinds of values they can store. These are called data types.
Ferret comes with a set of built‑in types that let you work with numbers, text, true/false values, and more.
Primitive Types
Section titled “Primitive Types”Primitive types are the simplest kinds of data. Internally they are just numbers. Unlike other languages, Ferret has a rich set of primitive types to give you more control over how data is stored and manipulated. We support maximum 256-bit integers and floating-point numbers with up to 71 decimal digits of precision! And it’s built right into the language without needing any special libraries.
Integer Types
Section titled “Integer Types”These types store whole numbers.
| Type | Size | Range | Description |
|---|---|---|---|
i8 | 8‑bit | -2⁷ to 2⁷‑1 | Small integer |
i16 | 16‑bit | -2¹⁵ to 2¹⁵‑1 | Medium integer |
i32 | 32‑bit | -2³¹ to 2³¹‑1 | Standard integer |
i64 | 64‑bit | -2⁶³ to 2⁶³‑1 | Bigger integer |
i128 | 128‑bit | -2¹²⁷ to 2¹²⁷‑1 | Very big integer |
i256 | 256‑bit | -2²⁵⁵ to 2²⁵⁵‑1 | Extremely big integer |
u8 | 8‑bit | 0 to 2⁸‑1 | Non‑negative small integer |
u16 | 16‑bit | 0 to 2¹⁶‑1 | Non‑negative medium integer |
u32 | 32‑bit | 0 to 2³²‑1 | Non‑negative integer |
u64 | 64‑bit | 0 to 2⁶⁴‑1 | Bigger non‑negative integer |
u128 | 128‑bit | 0 to 2¹²⁸‑1 | Very big non‑negative integer |
u256 | 256‑bit | 0 to 2²⁵⁶‑1 | Extremely big non‑negative integer |
Now if you are confused about the i and u prefixes, i stands for signed integers (can be negative) and u stands for unsigned integers (non-negative only). And the numbers 32 and 64 stand for the number of bits used to store the value. Other languages may use different names for these types, but the concepts are the same. So when you see i32, think of it as a 32-bit signed integer.
Now remember the := operator you learned about in the Variables & Constants section? It is used for declaring variables and constants with type inference. Type inference means Ferret can automatically figure out the type based on the value you provide. But if you want to explicitly specify the type, you can do so using a colon : followed by the type name.
let count: i32 = 42;let small: i8 = -128;let big_number: i64 = 9223372036854775807;let positive: u32 = 4294967295;let very_big: u128 = 340282366920938463463374607431768211455;let huge: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935;Floating‑Point Types
Section titled “Floating‑Point Types”These types store numbers with decimal points. Think of them as numbers that can have fractional parts.
| Type | Size | Precision | Description |
|---|---|---|---|
f32 | 32‑bit | ~7 digits | Single precision float |
f64 | 64‑bit | ~15 digits | Double precision (default) |
f128 | 128‑bit | ~34 digits | Quadruple precision float |
f256 | 256‑bit | ~71 digits | Octuple precision float |
The f stands for floating-point, and the numbers 32 and 64 represent the bits used to store the value. The bigger the number, the more precise your decimal calculations will be.
When you write a number with a decimal point without specifying a type, Ferret automatically uses the default f32 because it gives you better precision. But if the value requires more precision, it will promote it to f64 and so on.
let pi: f32 = 3.14159;let e: f64 = 2.718281828459045;let price := 19.99; // Inferred as f64let large_value: f128 = 1.2345678901234567890123456789012345;let precise_value: f256 = 1.2345678901234567890123456789012345678901234567890123456789012345678901234567890;String Type
Section titled “String Type”Strings store text - anything from single letters to entire paragraphs. In Ferret, strings are represented by the str type.
You create strings by wrapping text in double quotes ".
let name: str = "Ferret";let greeting: str = "Hello, World!";let emoji: str = "🦦"; // Strings support Unicode, including emojis!
// Strings can span multiple lineslet multiline: str = "HelloWorld";Strings are one of the most common types you’ll work with. They’re perfect for storing names, messages, file paths, and any other text data.
Strings are indexable. Indexing returns a byte (not a str), is byte-based (not Unicode code points), and uses runtime bounds checks. Negative indices count from the end (-1 is last byte):
let s: str = "Hello";let first: byte = s[0];let last: byte = s[-1]; // last bytelet n: i32 = len(s);Character Type
Section titled “Character Type”A character represents a single letter, symbol, or emoji. Unlike strings that can hold multiple characters, the byte type holds exactly one character. It is called byte because it typically uses one byte (8 bits) of memory to store the value. Internally byte and u8 are the same.
Characters are created using single quotes ' instead of double quotes.
let letter: byte = 'A';let newline: byte = '\n'; // Special characters use backslashThink of a byte as a single building block, while a str (string) is like a sequence of these blocks.
When printing, byte displays as a character while u8 prints as a number.
Boolean Type
Section titled “Boolean Type”Booleans represent yes/no, on/off, or true/false values. There are only two possible values: true and false.
The type name is bool, and booleans are essential for making decisions in your code.
let is_active: bool = true;let is_complete: bool = false;let has_permission := true; // Inferred as boolYou’ll use booleans constantly when writing conditions, like “if the user is logged in” or “while the game is running.”
Compound Types
Section titled “Compound Types”Compound types are built by combining other types together. They let you group related data.
Arrays
Section titled “Arrays”Arrays are collections that store multiple values of the same type in a specific order. Think of them as numbered containers where each slot holds one value.
There are two kinds of arrays in Ferret:
Dynamic arrays automatically grow when you add more elements. Use the append() builtin function to append an item to the end of an array.
let numbers: []i32 = [1, 2, 3, 4, 5];let names: []str = ["Alice", "Bob", "Charlie"];let scores := [95, 87, 92]; // Inferred as []i32
// Dynamic arrayslet arr := [1, 2, 4]; // size 3append(&mut arr, 43); // Append using mutable referenceNotice the [] before the type - this means “an array of” that type. Dynamic arrays have no bounds checking - they grow to accommodate any index you use.
Learn more: See the Built-in Functions documentation for complete details on array operations like get(), set(), insert(), and more.
Fixed-size arrays have a set number of elements that cannot change:
let days: [7]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];let coordinates: [3]f64 = [1.0, 2.5, 3.7];The number in brackets [7] tells you exactly how many items the array holds. Fixed-size arrays have compile-time bounds checking for constant indices:
let arr: [5]i32 = [1, 2, 3, 4, 5];let x := arr[2]; // OK - index 2 is validlet y := arr[10]; // Compile error: constant index 10 is out of bounds!
// Runtime indices panic if out of boundslet i := 10;let z := arr[i]; // Runtime panic: index out of bounds!Negative Indexing
Section titled “Negative Indexing”Both array types support negative indices to access elements from the end:
let numbers: [5]i32 = [10, 20, 30, 40, 50];let last := numbers[-1]; // 50 (last element)let second_last := numbers[-2]; // 40 (second to last)Negative indices count backwards: -1 is the last element, -2 is second to last, and so on. For fixed-size arrays, out-of-bounds negative constant indices are caught at compile-time:
let arr: [5]i32 = [1, 2, 3, 4, 5];let valid := arr[-5]; // OK - first elementlet invalid := arr[-6]; // Compile error: constant index -6 is out of bounds!
// Runtime negative indices panic if out of boundslet i := -10;let unsafe := arr[i]; // Runtime panic: index out of bounds!Important: For safe array access without panics, use the built-in functions:
get(&arr, index)- ReturnsT?(optional),noneif out of boundsget_or(&arr, index, fallback)- ReturnsTwith fallback if out of boundshas(&arr, index)- Returnsboolto check if index is valid
Array Safety Summary
Section titled “Array Safety Summary”| Array Type | Constant Indices | Runtime Indices | Negative Indexing |
|---|---|---|---|
Fixed-size [N]T | ✅ Compile error if out of bounds | ❌ Runtime panic if out of bounds | ✅ Same behavior |
Dynamic []T | ❌ Runtime panic if out of bounds | ❌ Runtime panic if out of bounds | ✅ Same behavior |
Both fixed-size and dynamic arrays panic on out-of-bounds access at runtime. Use the built-in functions (get(), get_or(), has()) for safe access without panics.
Optional Types
Section titled “Optional Types”Sometimes you need to represent “I might have a value, or I might not.” That’s what optional types do.
You make any type optional by adding a question mark ? after it. An optional type T? can hold either a value of type T or none. The none keyword is a constant (like true and false) that represents the absence of a value.
let maybe_number: i32? = 42; // Has a value (42)let no_value: str? = none; // No value (none is a constant like true/false)let age: i32? = none; // Starts with no valueOptional types help prevent bugs. Instead of crashing when something is missing, Ferret forces you to check if a value exists before using it.
let username: str? = get_username();
if username != none { // Safe to use username here io::Println("Hello, " + username);} else { io::Println("No username provided");}This is much safer than many other languages where missing values can cause crashes!
Maps are collections that store key-value pairs. Think of them like a dictionary where you look up values using keys instead of positions.
Unlike arrays which use numbers (0, 1, 2…) to access elements, maps let you use any type as a key - strings, numbers, or even custom types!
let scores: map[str]i32 = { "alice" => 95, "bob" => 87, "charlie" => 92} as map[str]i32;
let prices: map[str]f64 = { "apple" => 1.99, "banana" => 0.99} as map[str]f64;The syntax map[KeyType]ValueType tells Ferret what types the keys and values should be.
Accessing Map Values
Section titled “Accessing Map Values”Ferret provides two ways to access map values:
Direct indexing map[key] returns the value type T directly, but panics if the key doesn’t exist:
let ages := {"alice" => 25, "bob" => 30} as map[str]i32;
// Returns i32 directly (not i32?)let alice_age: i32 = ages["alice"]; // ✅ 25let missing: i32 = ages["unknown"]; // ❌ Panic: key not found!Safe access with the get() builtin returns an optional type:
let ages := {"alice" => 25, "bob" => 30} as map[str]i32;
// Returns i32? (optional i32) - key might not exist!let alice_age: i32? = get(&mut ages, "alice"); // Returns i32? with value 25let missing: i32? = get(&mut ages, "unknown"); // Returns i32? with value noneThis is a safety feature! It forces you to think about what happens when a key doesn’t exist, preventing crashes.
The Coalescing Operator with Maps
Section titled “The Coalescing Operator with Maps”The coalescing operator ?? is perfect for providing default values with get():
let scores := {"alice" => 95} as map[str]i32;
let alice_score := get(&mut scores, "alice") ?? 0; // 95let bob_score := get(&mut scores, "bob") ?? 0; // 0 (key doesn't exist)Or use the get_or() builtin for a more concise syntax:
let alice_score := get_or(&mut scores, "alice", 0); // 95let bob_score := get_or(&mut scores, "bob", 0); // 0 (fallback)This pattern is so common you’ll use it all the time when working with maps!
Learn more: Maps are covered in detail in the Type System section, and see Built-in Functions for all container operations.
Reference Types
Section titled “Reference Types”Reference types let you pass data by reference rather than by copy. Add & before a type to make it a reference:
type LargeData struct { .buffer: [1000]i32, .metadata: str,};
// Passes by copy (copies entire struct)fn process_copy(data: LargeData) { }
// Passes by reference (only copies pointer)fn process_ref(data: &LargeData) { }References are useful for:
- Avoiding expensive copies of large data
- Sharing data between functions
Learn more: References are covered in detail in the Type System section.
Custom Types
Section titled “Custom Types”Ferret lets you define your own types beyond the built-in ones. The most common custom types are structs, enums, and interfaces.
Defining Custom Types
Section titled “Defining Custom Types”You use the type keyword to define new structured types:
// Define a struct typetype Point struct { .x: f64, .y: f64};
// Define an enum typetype Color enum { Red, Green, Blue};
let point: Point = { .x: 10.0, .y: 20.0 } as Point;let color: Color = Color::Red;These custom types make your code more organized and type-safe. We’ll dive deeper into structs, enums, and interfaces in the Type System section.
Strict Type Checking
Section titled “Strict Type Checking”Ferret enforces strict type matching for all numeric operations, similar to Rust. This means you cannot mix different numeric types in arithmetic operations without explicit casting.
Why Strict Typing?
Section titled “Why Strict Typing?”- No Surprises: You always know exactly what type you’re working with
- Performance: No hidden runtime conversions or checks
- Safety: Prevents accidental truncation or precision loss
- Predictability: The type system is simple and deterministic
- Explicit Intent: Casts document your intentions in code
All Operands Must Match
Section titled “All Operands Must Match”When performing arithmetic operations, both operands must be the same type, and the result will be that same type:
let a: i32 = 100;let b: i256 = 340282366920938463463374607431768211456;
// ❌ ERROR: mismatched types in arithmetic: i32 and i256// let result := a + b;
// ✅ CORRECT: Explicit cast requiredlet result := (a as i256) + b; // result is i256This applies to all arithmetic operators: +, -, *, /, %, and **.
Mixing Integer and Float Types
Section titled “Mixing Integer and Float Types”You cannot mix integers and floats without explicit casting:
let integer: i32 = 42;let floating: f64 = 3.14159;
// ❌ ERROR: mismatched types// let sum := integer + floating;
// ✅ CORRECT: Cast integer to floatlet sum := (integer as f64) + floating; // result is f64Different Sized Integers
Section titled “Different Sized Integers”Even integers of different sizes require explicit casting:
let small: i32 = 100;let large: i64 = 9223372036854775807;
// ❌ ERROR: mismatched types// let result := small + large;
// ✅ CORRECT: Cast to matching typelet result := (small as i64) + large; // result is i64
// Or cast down (with potential overflow):let result2 := small + (large as i32); // result is i32Different Sized Floats
Section titled “Different Sized Floats”The same rule applies to floating-point types:
let f32val: f32 = 3.14;let f64val: f64 = 2.718281828;
// ❌ ERROR: mismatched types// let result := f32val + f64val;
// ✅ CORRECT: Cast to matching typelet result := (f32val as f64) + f64val; // result is f64Power Operator
Section titled “Power Operator”The power operator (**) follows the same strict typing rules:
let base: i256 = 2;let exp: i32 = 10;
// ❌ ERROR: mismatched types// let result := base ** exp;
// ✅ CORRECT: Cast to matching typelet result := base ** (exp as i256); // result is i256Type Conversion
Section titled “Type Conversion”Ferret requires explicit type conversions for all numeric operations. Use the as keyword to cast between types.
Casting Between Number Types
Section titled “Casting Between Number Types”Use the as keyword to convert between number types:
let small: i32 = 42;let big: i64 = small as i64; // Convert to bigger integer
let whole: i32 = 100;let decimal: f64 = whole as f64; // Convert to floating-point
let pi: f64 = 3.14159;let rounded: i32 = pi as i32; // Becomes 3 (decimal part removed)Common Casting Patterns
Section titled “Common Casting Patterns”Widening (Safe - No Data Loss):
let small: i32 = 100;let large := small as i64; // i32 → i64 (safe)let huge := small as i256; // i32 → i256 (safe)
let f32val: f32 = 3.14;let f64val := f32val as f64; // f32 → f64 (safe)Narrowing (Unsafe - Potential Data Loss):
let large: i64 = 9223372036854775807;let small := large as i32; // i64 → i32 (truncates!)
let precise: f64 = 3.14159265358979;let lessPrec := precise as f32; // f64 → f32 (loses precision)Converting To and From Strings
Section titled “Converting To and From Strings”There is no built-in way to convert between strings and other types yet. This is done via standard library functions which will be covered later.
Summary
Section titled “Summary”You’ve learned about Ferret’s type system! Here’s what we covered:
- Primitive types: Integers (
i32,i64,u32,u64), floats (f32,f64), strings (str), booleans (bool), and characters (byte) - Compound types: Arrays and maps that hold multiple values
- Optional types: Types that can be a value or
none - Reference types: Pass data by reference with
&T - Type inference: Letting Ferret figure out types automatically
- Type conversion: Explicitly changing between types
Next Steps
Section titled “Next Steps”Now that you know about types, you’re ready to learn what you can do with them:
- Learn about Operators - Do math, compare values, and more
- Explore Optional Types in depth - Master safe handling of missing values
- Understand Structs - Create your own custom types