Type Compatibility and Casting
Ferret has a strict type system that ensures type safety while allowing convenient implicit conversions where safe. This page explains the rules for type compatibility and when explicit casting is required.
Type Compatibility Levels
Section titled “Type Compatibility Levels”Ferret classifies type assignments into three categories:
- Identical: Types are exactly the same and can be assigned without any conversion
- Implicitly Castable: Types can be safely converted without losing information
- Explicitly Castable: Conversion is possible but requires explicit casting due to potential information loss
Casting Syntax
Section titled “Casting Syntax”When explicit casting is required, Ferret uses the as keyword:
let source_value: SourceType = get_value();let target_value: TargetType = source_value as TargetType;Primitive Type Conversions
Section titled “Primitive Type Conversions”Integer Types
Section titled “Integer Types”Ferret supports implicit widening conversions between integer types (no information loss):
let a: i8 = 10;let b: i16 = a; // Implicit: i8 -> i16 (widening)let c: i32 = b; // Implicit: i16 -> i32 (widening)Reverse conversions (narrowing) can lose data, so they require explicit casting:
let x: i32 = 1000;let y: i16 = x as i16; // Explicit cast: potential data loss acknowledgedFloating Point Types
Section titled “Floating Point Types”Similar rules apply to floating point types:
let f32_val: f32 = 1.5;let f64_val: f64 = f32_val; // Implicit: f32 -> f64let f128_val: f128 = f64_val; // Implicit: f64 -> f128Cross-Type Conversions
Section titled “Cross-Type Conversions”Conversions between integers and floats can lose precision or change representation, so they require explicit casting to make the intent clear:
let int_val: i32 = 42;let float_val: f64 = int_val as f64; // Explicit cast: integer to float
let float_val: f64 = 3.14;let int_val: i32 = float_val as i32; // Explicit cast: float to integer (truncates decimal part)Named Types
Section titled “Named Types”Named types provide type safety by distinguishing semantically different types, even when they have the same underlying representation.
Base to Named
Section titled “Base to Named”Base types can be implicitly assigned to compatible named types:
type Integer i32;type Float f64;
let base_int: i32 = 42;let named_int: Integer = base_int; // Implicit: base -> named
let base_float: f64 = 3.14;let named_float: Float = base_float; // Implicit: base -> namedNamed to Base
Section titled “Named to Base”Converting from named types back to base types loses the type distinction, so explicit casting is required:
type Integer i32;
let named_val: Integer = 42;let base_val: i32 = named_val as i32; // Explicit cast: loses type informationNamed to Named
Section titled “Named to Named”Even if two named types have the same underlying type, they represent different semantic concepts and require explicit casting to prevent accidental mixing:
type Count i32;type Index i32;
let count: Count = 10;let index: Index = count as Index; // Explicit cast: different semantic typesUnion Types
Section titled “Union Types”Assigning to Unions
Section titled “Assigning to Unions”Values can be implicitly assigned to union types if they match one of the variants:
type Result union { i32, str };
let success: i32 = 42;let result: Result = success; // Implicit: i32 is a variant
let error: str = "failed";let result2: Result = error; // Implicit: str is a variantNamed Unions
Section titled “Named Unions”Named union types work the same way:
type MyUnion union { i32, str };
let val: i32 = 100;let union_val: MyUnion = val; // ImplicitType Narrowing with is
Section titled “Type Narrowing with is”The is operator allows checking and narrowing union types:
type Result union { i32, str };
fn process(result: Result) { if result is i32 { // Inside this block, result is narrowed to i32 let value: i32 = result; // Valid io::Println("Success: {}", value); } else { // result is narrowed to str let message: str = result; // Valid io::Println("Error: {}", message); }}See Union Types for full syntax and usage.
Special Cases
Section titled “Special Cases”Untyped Literals
Section titled “Untyped Literals”Untyped integer and float literals can be implicitly assigned to compatible types:
let int_var: i32 = 42; // Implicit: untyped int -> i32let float_var: f64 = 3.14; // Implicit: untyped float -> f64
// But not to incompatible types:let wrong: f64 = 42; // Error: untyped int cannot be assigned to f64Optional Types
Section titled “Optional Types”The none value can be implicitly assigned to any optional type:
let optional_int: i32? = none; // Implicitlet optional_str: str? = none; // ImplicitReferences
Section titled “References”Reference types have specific compatibility rules:
let x: i32 = 42;let ref_x: &i32 = &x;
let y: &mut i32 = &mut x; // Mutable referencelet z: &i32 = &x; // Immutable referencelet w: &i32 = y; // Error: mutability must match (no implicit &mut -> &)Common Pitfalls
Section titled “Common Pitfalls”-
Assuming named types are interchangeable: Even with the same underlying type, different named types require casting.
-
Forgetting explicit casts for narrowing: Converting from wider to narrower types always requires explicit casting.
-
Untyped literals in mixed contexts: Untyped literals cannot be assigned to incompatible types.
-
Union narrowing: After type narrowing with
is, the compiler knows the narrowed type, but you must use it correctly.
Best Practices
Section titled “Best Practices”- Use named types to create domain-specific types for better code clarity and safety
- Be explicit about casts when converting between different numeric representations
- Leverage type narrowing with unions to write safer, more expressive code
- Use the compiler’s error messages to guide when explicit casts are needed
Remember: Ferret prioritizes safety - if a conversion could lose information or violate type safety, it requires explicit casting.