Type Generator Functions
In Osta, types are first-class citizens during compilation, meaning they can be manipulated like any other entity within
compile-time contexts. This design replaces traditional generics with type generator functions—compile-time functions
that produce types based on arbitrary arguments. For generic compound types, Osta provides a synthetic #type function
in the type’s namespace, serving as a default type constructor. This document explains type generator functions, their
representation, and their usage in Osta, covering both standalone functions and those tied to generic structs.
What’s a Generic Type?
A generic type in Osta is a type parameterized by other types or values, enabling reusable, type-safe code. At its core,
these types are the output of type generator functions, which are compile-time functions that produce types. For
example, a Map type parameterized by key and value types (e.g., Map<i32, f64>) is generated by a compile-time
function that constructs a struct tailored to those parameters. This approach leverages Osta’s type system, where types
are first-class during compilation, allowing flexible type constructions.
Generic types appear in code with familiar syntax (e.g., Map<K, V>), but internally, they result from invoking type
generator functions, often with syntactic sugar provided by the compiler. These types integrate with Osta’s features,
such as type inference and lifetime management, and are used in variable declarations, function signatures, or further
compile-time computations.
How Osta’s Compiler Represents Generic Types
Osta’s compiler represents generic types as the output of compile-time functions, executed during compilation to produce
concrete type definitions. When a generic type like Map<i32, f64> is used, the compiler invokes the associated type
generator function (e.g., Map::#type(i32, f64)) to construct the type. This function defines the type’s structure,
such as a struct with fields tailored to the provided parameters.
For compound types (e.g., structs, tagged unions), the compiler automatically provides a synthetic #type function in
the type’s namespace unless overridden. This function acts as a default constructor, generating the type based on the
provided arguments. The compiler translates syntactic sugar (e.g., Map<K, V>) into calls to these functions, ensuring
type safety and consistency.
Osta’s compiler does not currently cache type constructions. Each use of a generic type, even with identical parameters, re-evaluates the type generator function, potentially increasing compile times for complex types or frequent instantiations. Future versions may implement caching to improve performance.
To mitigate this, manual monomorphization is highly recommended. Create a monomorphization file to predefine specific type instances, like so:
// Predefine a Map type for i32 keys and f64 values
pub const IntFloatMap: type = Map<i32, f64>;
// Equivalent to Map::#type(i32, f64)
Creating a Compile-Time Function to Generate a Type
A type generator function is a compile-time function with a name starting with # (e.g., #fixedSizeArray), which
treats types as first-class citizens, allowing them to be manipulated like other values. These functions can take
arbitrary arguments, such as types, constants, or other values, and return a type. They can be defined standalone or
within a type’s namespace and are executed at compile-time to produce type definitions.
Syntax:
pub fn #functionName(arg1: T1, arg2: T2, ...) -> type {
// Return a type definition
}
Example:
pub fn #fixedSizeArray(T: type, size: usize) -> type {
[T; size]
}
fn main() {
let arr: #fixedSizeArray(i32, 5) = [1, 2, 3, 4, 5];
print(arr[0]); // Outputs: 1
}
In this example, #fixedSizeArray takes a type T and a usize constant size, returning a fixed-size array type
[T; size]. The function is standalone, demonstrating that compile-time functions can accept non-type arguments, such
as usize, for flexible type generation. Types are treated as first-class within the function, enabling their
manipulation alongside other arguments.
Creating a Generic Struct
A generic struct is a compound type parameterized by other types or values, defined with a struct declaration that
includes type parameters. The compiler automatically provides a synthetic #type function in the struct’s namespace to
generate the type based on these parameters, unless a custom implementation is provided.
Example:
struct Map<K: type, V: type> {
keys: *K,
values: *V,
size: usize,
}
impl<K: type, V: type> Map<K, V> {
pub fn new() -> Map<K, V> {
Map<K, V> { keys: 0, values: 0, size: 0 }
}
}
fn main() {
let map: Map<i32, f64> = Map<i32, f64>::new();
print(map.size); // Outputs: 0
}
Here, Map<K, V> is a generic struct parameterized by K and V. The compiler provides a default
Map::#type(K: type, V: type) function that constructs the struct type. The Map<i32, f64> syntax is sugar for
invoking Map::#type(i32, f64), and the new method creates an instance of the generated type.
Adding Custom Type Constructors
Developers can define custom type constructors by implementing a #type function in the struct’s impl block,
overriding the default synthetic #type function. This allows tailored type generation, such as adding fields or
enforcing constraints, with types treated as first-class citizens within the function.
Example:
struct ConstrainedArray<T: type, S: usize, Min: usize, Max: usize> {
data: [T; S],
size: usize,
}
impl ConstrainedArray {
pub fn #type(T: type, S: usize, Min: usize, Max: usize) -> type {
if S < Min || S > Max {
std::comptime::#error("Size must be within the specified bounds.");
}
struct {
data: [T; S],
size: usize,
}
}
}
impl<T: type, S: usize, Min: usize, Max: usize> ConstrainedArray<T, S, Min, Max> {
pub fn new(data: [T; S]) -> ConstrainedArray<T, S, Min, Max> {
ConstrainedArray<T, S, Min, Max> { data, size: S }
}
}
fn main() {
let arr: ConstrainedArray<i32, 2, 0, 5> = ConstrainedArray<i32, 2, 0, 5>::new([0, 1]);
print(arr.size); // Outputs: 2
}
In this example, the custom ConstrainedArray::#type function adds constraints on the size of the array, showing how
developers can extend the default type construction while manipulating types as first-class entities.
Forbidding the Default Type Constructor
To prevent the use of the synthetic #type function, developers can define a custom #type function that triggers a
compile-time error when invoked with unsupported arguments. This ensures that only specific, custom type constructors
are used.
Example:
struct Map<K: type, V: type> {
keys: *K,
values: *V,
size: usize,
}
impl Map {
pub fn #type(K: type, V: type) -> type {
std::comptime::#error("Default #type constructor is forbidden. Use Map::#type(W, K, V) instead.");
}
pub fn #type(W: fn(T: type) -> type, K: type, V: type) -> type {
struct {
keys: *W<K>,
values: *W<V>,
size: usize,
}
}
}
struct Wrapper<T: type> {
value: T,
}
impl<K: type, V: type> Map<Wrapper<K>, Wrapper<V>> {
pub fn new() -> Map<Wrapper<K>, Wrapper<V>> {
Map<Wrapper<K>, Wrapper<V>> { keys: 0, values: 0, size: 0 }
}
}
fn main() {
// let map: Map<i32, f64> = Map<i32, f64>::new(); // Compile error: Default #type forbidden
let map: Map<Wrapper<i32>, Wrapper<f64>> = Map<Wrapper::#type, i32, f64>::new();
print(map.size); // Outputs: 0
}
Here, the default Map::#type function is overridden to throw a compile-time error, forcing the use of
Map::#customType, which constructs a Map with wrapped types.