Reflection
Reflection provides the ability for a program to introspect itself, and perform operations different dynamically at runtime based on a values type, or metadata in the compiled program.
In Onyx, reflection is available through the runtime.info
package.
This package provides utility function for accessing all type information
and metadata (tags) stored in the binary.
Types are Values
Every type in Onyx is given a unique ID at compile time. This ID is not stable, so a separate compilation may choose a different ID for the same nominal type. By having a single integer for every type, Onyx's types can be runtime values as well as compile time values.
In the example, t
is variable that stores a type.
main :: () {
t := i32;
println(t); // Prints i32
println(typeof t); // Prints type_expr, aka the type of a type
}
Under the hood, t
is simply storing a 32-bit integer that is the
unique ID of i32
.
any
This ability to have types as runtime values enables any
in Onyx.
any
is a dynamically typed value, whose type is known at runtime,
instead of at compile-time. Under the hood, any
looks like this:
any :: struct {
data: rawptr;
type: type_expr;
}
As you can see, it stores a data pointer and a runtime-known type.
Every any
points to a region of memory where the value is actually stored.
You can think of any
like a "fat-pointer" that stores the pointer, plus the type.
any
is typically used as an argument type on a procedure.
When a parameter has any
as its type, the compiler will implicitly
wrap the corresponding argument in an any
, placing the argument on the stack,
and constructing an any
using the pointer to the stack and the type of the
argument provided.
uses_any :: (value: any) {
println(value.type);
}
main :: () {
uses_any(10); // Prints i32
uses_any("Hello"); // Prints str
uses_any(context); // Prints OnyxContext
}
any
can also be used for variadic arguments of different types.
/// Prints all of the
many_args :: (values: ..any) {
for value in values {
printf("{} ", value.type);
}
}
main :: () {
many_args(10, "Hello", context);
// Prints: i32 str, OnyxContext
}
To use the data inside of an any
, you have to write code that handles the different
types, or kinds of types that you expect. You can either check for concrete types explicitly,
or use runtime type information to handle things dynamically. To get the type information for a given type,
use the runtime.info.get_type_info
procedure, of the info
method on the type_expr
.
print_size :: (v: any) {
size := switch v.type {
case i32 => 4
case i64 => 8
case str => 8
case _ => -1
};
printf("{} is {} bytes.\n", v.type, size);
}
main :: () {
print_size(10);
print_size("Hello");
print_size(context);
}
In this contrived example, print_size
checks the type of the any
against explicit
types using a switch expression, defaulting to -1 if the type is not one of them.
For some applications of any
this is perfectly acceptable, but for others, a more
generalized approach might be necessary. In such cases, you can use runtime type information to introspect the type.
Using Runtime Type Information
Baked into every Onyx compilation is a type table. This table contains information on every type in the Onyx program, from the members of structures, to the variants of unions, to which polymorphic structure was used to create a structure.
This information is stored in runtime.info.type_table
, which is a slice that contains
a &Type_Info
for each type in the program.
Type_Info
stores generic information for every type, such as the size.
When given a &Type_Info
, you will generally cast it to another type to get more
information out of it by using the kind
member.
In this example, when a structure type is passed in, the function will print the all of the members of the structure, including their: name, type and offset.
print_struct_details :: (type: type_expr) {
info := type->info();
struct_info := info->as_struct(); // OR cast(&Type_Info_Struct) info
for member in struct_info.members {
printf("Member name : {}\n", member.name);
printf("Member type : {}\n", member.type);
printf("Member offset : {} bytes\n", member.offset);
printf("\n");
}
}
Foo :: struct {
first: str;
second: u32;
third: &Foo;
}
main :: () {
print_struct_details(Foo);
}
This prints:
Member name : first
Member type : [] u8
Member offset : 0 bytes
Member name : second
Member type : u32
Member offset : 8 bytes
Member name : third
Member type : &Foo
Member offset : 12 bytes
In this example, runtime type information is used to get the size of the type.
print_size :: (v: any) {
info := v.type->info();
size := info.size; // Every type has a known size
printf("{} is {} bytes.\n", v.type, size);
}
main :: () {
print_size(10);
print_size("Hello");
print_size(context);
}