- Type System
- Built-in Types
- Built-in Error Types
- Type Inference Rules
- Type Safety and Explicit Typing
- Any Type Handling and Pattern Matching Requirement
- Forbidden Operations on
any
Types - Legal Operations on
any
Types - Pattern Matching Requirement
- Direct Access Compilation Errors
- Function Return Type Handling
- Type Annotation Pattern Syntax
- Compilation Error Messages
- Exhaustiveness Checking for Any Types
- Default Wildcard Behavior for Any Types
- Type Constraint Checking
- Context-Aware Type Validation
- Compilation Errors for Impossible Types
- Performance and Safety Characteristics
- Type Annotation Requirements
- Compilation Errors for Type Ambiguity
- Error Handling Requirements
- Forbidden Operations on
- Type Compatibility
5. Type System #
Ospreyβs type system is one of the most important aspects of the language. It puts type safety and expressiveness as the top priorities. It is inspired by popular functional programming languages like Haskell and ML. The type system aims towards making illegal states unrepresentable. Some features like constructor where constraints are critical features that mean that instances cannot exist where they fail the criteria in construction.
5.1 Built-in Types #
Int
: 64-bit signed integersString
: UTF-8 encoded stringsBool
: Boolean values (true
,false
)Unit
: Type for functions that donβt return a meaningful valueResult<T, E>
: Built-in generic type for error handlingList<T, N>
: Immutable fixed-size lists with N elements of type TFunction Types
: First-class function types with syntax(T1, T2, ...) -> R
Function Types #
Function types represent functions as first-class values, enabling higher-order functions and function composition.
Syntax:
FunctionType := '(' (Type (',' Type)*)? ')' '->' Type
Examples:
(Int) -> Int // Function taking an Int, returning an Int
(Int, String) -> Bool // Function taking Int and String, returning Bool
() -> String // Function with no parameters, returning String
(String) -> (Int) -> Bool // Higher-order function returning another function
Function Type Declarations:
// Function parameter with explicit function type
fn applyFunction(value: Int, transform: (Int) -> Int) -> Int =
transform(value)
// Variable with function type
let doubler: (Int) -> Int = fn(x: Int) -> Int = x * 2
// Higher-order function that returns a function
fn createAdder(n: Int) -> (Int) -> Int =
fn(x: Int) -> Int = x + n
Function Composition Examples:
// Define some simple functions
fn double(x: Int) -> Int = x * 2
fn square(x: Int) -> Int = x * x
fn addFive(x: Int) -> Int = x + 5
// Higher-order function with strong typing
fn applyTwice(value: Int, func: (Int) -> Int) -> Int =
func(func(value))
// Usage with type safety
let result1 = applyTwice(5, double) // 20
let result2 = applyTwice(3, square) // 81
let result3 = applyTwice(10, addFive) // 20
// Composition of functions
fn compose(f: (Int) -> Int, g: (Int) -> Int) -> (Int) -> Int =
fn(x: Int) -> Int = f(g(x))
let doubleSquare = compose(double, square)
let result4 = doubleSquare(3) // double(square(3)) = double(9) = 18
Type Safety Benefits:
- Compile-time validation: Function signatures are checked at compile time
- No runtime type errors: Mismatched function types caught early
- Clear documentation: Function types serve as documentation
- Enables optimization: Compiler can optimize based on known function signatures
5.2 Built-in Error Types #
MathError
: For arithmetic operations (DivisionByZero, Overflow, Underflow)ParseError
: For string parsing operationsIndexError
: For list/string indexing operations (OutOfBounds)Success
: Successful result wrapper
5.3 Type Inference Rules #
Core Principle: The any
type is invalid unless explicitly declared. All types must be either explicitly annotated or inferrable from context.
Function Return Types #
Return type annotations may be omitted only when the return type can be definitively inferred from the function body:
Allowed (Return Type Inferred)
- Literal expressions:
fn getNumber() = 42
β infersint
- String literals:
fn getText() = "hello"
β infersstring
- Boolean literals:
fn getFlag() = true
β infersbool
- Arithmetic expressions:
fn calculate() = 1 + 2
β infersint
Disallowed (Requires Explicit Return Type)
- Direct parameter return:
fn identity(x) = x
β ERROR (would beany
) - Function calls:
fn process() = someFunction()
β ERROR (unknown return type) - Complex expressions: Without clear type resolution
Parameter Types #
Parameter type annotations may be omitted only when the parameter type can be definitively inferred from usage within the function body:
Allowed (Parameter Type Inferred)
- Arithmetic usage:
fn addOne(x) = x + 1
βx
infers asint
- With explicit return type:
fn identity(x) -> int = x
βx
infers asint
from return type - Direct return with explicit type:
fn process(data) -> string = data
βdata
infers asstring
Disallowed (Requires Explicit Parameter Type)
- Direct return without return type:
fn identity(x) = x
β ERROR - String parameter return:
fn greet(name) = name
β ERROR (usename: string
or-> string
) - Mixed parameter scenarios:
fn formatScore(name, score) = name
β ERROR - Ambiguous usage: Where type cannot be determined from context
Type Inference Examples #
Valid Code:
// Literals allow return type inference
fn getAge() = 25
fn getName() = "Alice"
fn isActive() = true
// Arithmetic allows both return and parameter type inference
fn increment(x) = x + 1
fn add(a, b) = a + b
// Explicit types always allowed
fn identity(x) -> int = x
fn process(data: string) -> string = data
Invalid Code:
// ERROR: Cannot infer return type from parameter
fn identity(x) = x
// ERROR: String parameter without type annotation
fn greet(name) = name
// ERROR: Mixed parameters without explicit types
fn formatScore(name, score) = name
// ERROR: Cannot infer parameter type from function call
fn process(data) = someFunction(data)
// ERROR: Ambiguous type inference
fn conditional(flag, a, b) = if flag then a else b
Rationale #
This design ensures:
- Type Safety: No implicit
any
types that could lead to runtime errors - Readability: Clear type contracts without excessive annotation
- Maintainability: Predictable type behavior for code evolution
- Performance: Compile-time type checking without runtime overhead
Summary Rule: βType annotations may be omitted only when the type can be unambiguously determined from constants, literals, well-defined operations, or explicit return types that constrain parameter types.β
Function Return Type βanyβ Restriction #
CRITICAL RULE: Functions CANNOT return any
type unless the return type is EXPLICITLY declared as any
.
β ALLOWED - Explicit any return type:
fn parseValue(input: string) -> any = processInput(input)
fn getDynamicValue() -> any = readFromConfig()
β FORBIDDEN - Implicit any return type:
fn identity(x) = x // ERROR: Would infer as 'any'
fn callUnknown() = someFunction() // ERROR: Would infer as 'any'
fn processData(data) = data // ERROR: Would infer as 'any'
Rationale: This prevents accidental any
type propagation that could lead to runtime type errors and maintains Ospreyβs strong type safety guarantees.
Built-in Functions: No built-in functions return any
type. All built-in functions have concrete, well-defined return types.
Common Validation Fixes #
When the compiler reports type inference errors, use these patterns:
// β ERROR: Function 'greet' requires explicit return type annotation
fn greet(name) = name
// β
FIX: Add explicit parameter type
fn greet(name: string) = name
// β
FIX: Add explicit return type
fn greet(name) -> string = name
// β ERROR: Parameter 'name' requires explicit type annotation
fn formatScore(name, score) = name
// β
FIX: Add explicit parameter types
fn formatScore(name: string, score: int) = name
// β
FIX: Add explicit return type to enable inference
fn formatScore(name, score) -> string = name
5.4 Type Safety and Explicit Typing #
CRITICAL RULE: Osprey is fully type-safe with no exceptions.
Mandatory Type Safety #
- No implicit type conversions - all type mismatches are compile-time errors
- No runtime type errors - all type issues caught at compile time
- No panics or exceptions - all error conditions must be handled explicitly
5.5 Any Type Handling and Pattern Matching Requirement #
π IMPLEMENTATION STATUS: any
type validation is partially implemented. Basic validation for function arguments is working, but complete pattern matching enforcement is in progress.
Osprey provides the any
type for maximum flexibility, but enforces strict access rules to maintain type safety. Direct access to any
types is forbidden in most contexts - all any
values must be accessed through pattern matching to extract their actual types.
Forbidden Operations on any
Types #
The following operations on any
types will result in compilation errors:
- Direct variable access -
any
variables cannot be used directly - Function arguments -
any
values cannot be passed to functions expecting concrete types - Field access - Properties cannot be accessed directly on
any
types - Implicit conversions -
any
cannot be implicitly converted to other types
Legal Operations on any
Types #
Arithmetic operations with any
types are explicitly allowed and return Result
types:
let x: any = 42
let result = x + 5 // Returns Result<Int, ArithmeticError>
let y: any = "hello"
let sum = y + 10 // Returns Result<Int, TypeError>
These operations are safe because they return Result
types that encapsulate potential errors, maintaining type safety while allowing flexible computation.
Pattern Matching Requirement #
Pattern Matching on any
Types:
// Pattern matching on any type
match anyValue {
value: Int => handleInteger(value)
value: String => handleString(value)
value: Bool => handleBoolean(value)
_ => handleUnknownType()
}
Direct Access Compilation Errors #
β FORBIDDEN - Direct Access:
fn processAny(value: any) -> int = value + 1
// ERROR: cannot use 'any' type directly in arithmetic operation
fn getLength(value: any) -> int = value.length
// ERROR: cannot access field on 'any' type without pattern matching
let result: int = someAnyFunction()
// ERROR: cannot assign 'any' to 'int' without pattern matching
fn callFunction(value: any) = someFunction(value)
// ERROR: cannot pass 'any' type to function expecting specific type
let converted = toString(value) // where value: any
// ERROR: cannot implicitly convert 'any' to expected parameter type
β REQUIRED - Pattern Matching:
fn processAny(value: any) -> int = match value {
num: Int => num + 1
str: String => length(str)
_ => 0
}
fn getLength(value: any) -> int = match value {
str: String => length(str)
arr: Array<T> => arrayLength(arr)
_ => 0
}
let result = match someAnyFunction() {
value: Int => value
_ => 0
}
Function Return Type Handling #
Functions returning any
types require immediate pattern matching:
// Function that returns any
extern fn parseValue(input: string) -> any
// ERROR: Direct usage
let number = parseValue("42") + 1
// CORRECT: Pattern matching
let number = match parseValue("42") {
value: Int => value + 1
_ => 0
}
Type Annotation Pattern Syntax #
The :
operator is used for type annotation in patterns:
value: Int
- Matches if value is an Int, binds tovalue
text: String
- Matches if value is a String, binds totext
flag: Bool
- Matches if value is a Bool, binds toflag
{ name, age }
- Structural match on any type with name and age fieldsperson: { name, age }
- Named structural match, binds whole object and fields_
- Wildcard matches any remaining type
Compilation Error Messages #
The compiler MUST emit these specific errors for any
type violations:
// Direct arithmetic operation
"cannot use 'any' type directly in arithmetic operation - pattern matching required"
// Direct field access
"cannot access field on 'any' type without pattern matching"
// Direct assignment to typed variable
"cannot assign 'any' to 'TYPE' without pattern matching"
// Direct function argument
"cannot pass 'any' type to function expecting 'TYPE' - pattern matching required"
// Implicit conversion attempt
"cannot implicitly convert 'any' to 'TYPE' - use pattern matching to extract specific type"
// Variable access on any
"cannot access variable of type 'any' directly - pattern matching required"
// Missing pattern match arm
"pattern matching on 'any' type must handle all possible types or include wildcard"
// Impossible type patterns
"pattern 'TYPE' is not a possible type for expression of documented types [TYPE1, TYPE2, ...]"
// Unreachable patterns
"unreachable pattern: 'TYPE' cannot occur based on context analysis"
Exhaustiveness Checking for Any Types #
Pattern matching on any
types MUST be exhaustive:
- Handle all expected types, OR
- Include a wildcard pattern (
_
) to handle unexpected types
// Non-exhaustive (ERROR)
match anyValue {
value: Int => processInt(value)
value: String => processString(value)
// ERROR: missing wildcard or Bool case
}
// Exhaustive (CORRECT)
match anyValue {
value: Int => processInt(value)
value: String => processString(value)
_ => handleOther()
}
Default Wildcard Behavior for Any Types #
The wildcard pattern (_
) in any
type matching preserves the any
type:
// Wildcard returns any type
let result = match someAnyValue {
value: Int => processInt(value) // Returns specific type
value: String => processString(value) // Returns specific type
_ => someAnyValue // Returns any type (unchanged)
}
// result type: any (due to wildcard arm)
// To avoid any type in result, handle all expected cases explicitly
let result = match someAnyValue {
value: Int => processInt(value)
value: String => processString(value)
_ => defaultInt() // Convert to specific type
}
// result type: Int (all arms return Int)
Type Constraint Checking #
The compiler MUST validate that pattern types are actually possible for the value being matched:
β VALID - Realistic Type Patterns:
// Function known to return Int or String
extern fn parseIntOrString(input: string) -> any
match parseIntOrString("42") {
value: Int => value + 1
value: String => length(value)
_ => 0 // Valid: handles any unexpected types
}
β INVALID - Impossible Type Patterns:
// Function documented to only return Int or String
extern fn parseIntOrString(input: string) -> any
match parseIntOrString("42") {
value: Int => value + 1
value: String => length(value)
value: Bool => if value then 1 else 0 // ERROR: Bool not possible
_ => 0
}
// ERROR: pattern 'Bool' is not a possible type for function 'parseIntOrString'
Context-Aware Type Validation #
When the compiler has information about possible types (from documentation, extern declarations, or analysis), it MUST enforce realistic pattern matching:
// Extern function with documented return types
extern fn getUserInput() -> any // Documentation: returns Int | String | Bool only
// VALID: Only realistic types
match getUserInput() {
value: Int => processInt(value)
value: String => processString(value)
value: Bool => processBool(value)
_ => handleUnexpected() // Still allowed for safety
}
// INVALID: Unrealistic types
match getUserInput() {
value: Int => processInt(value)
value: Array<String> => processArray(value) // ERROR: Array not documented
_ => handleOther()
}
// ERROR: pattern 'Array<String>' is not a documented return type for 'getUserInput'
Compilation Errors for Impossible Types #
"pattern 'TYPE' is not a possible type for expression of documented types [TYPE1, TYPE2, ...]"
"unreachable pattern: 'TYPE' cannot occur based on context analysis"
"pattern matching includes impossible type 'TYPE' - check function documentation"
Performance and Safety Characteristics #
- Compile-time type checking: Pattern matching enables compile-time verification
- Zero runtime cost: Type patterns compiled to efficient type tags
- Memory safety: No type confusion or invalid casts possible
- Explicit control: Developers must explicitly handle all type cases
Type Annotation Requirements #
When the compiler cannot infer types, explicit type annotations are REQUIRED:
// Type annotations required when inference is ambiguous
fn complexOperation(data: String, count: Int) = processData(data, count)
// Generic functions require type parameters
fn parseValue<T>(input: String) -> Result<T, ParseError> = ...
// Union types with fields require explicit typing
type Result<T, E> = Ok { value: T } | Err { error: E }
Compilation Errors for Type Ambiguity #
The compiler MUST emit errors when:
- Function parameter types cannot be inferred from usage
- Return types are ambiguous
- Variable types cannot be determined from initializers
- Generic type parameters are not specified
Error Handling Requirements #
- No exceptions or panics - all failing operations return Result types
- Explicit error handling - all Result types must be pattern matched
- Safe arithmetic - operations like division must return Result<T, Error>
// REQUIRED: Safe division that cannot panic
fn safeDivide(a: Int, b: Int) -> Result<Int, MathError> = match b {
0 => Err { error: DivisionByZero }
_ => Ok { value: a / b }
}
// REQUIRED: All results must be handled
let result = safeDivide(a: 10, b: 2)
match result {
Ok { value } => print("Result: ${value}")
Err { error } => handleError(error)
}
5.6 Type Compatibility #
- Pattern matching for type discrimination
- Union types for representing alternatives
- Result types for error handling instead of exceptions