Typing

Declaration

Constants
u :: "what";  
    // Untyped.
y : int : 123
    // Explicitly typed constant.
  • Consts are not indexable .

  • ::  is closer to #define  than it is static const .

  • To achieve similar behaviour to C’s static const , apply the @(rodata)  attribute to a variable declaration ( := ) to state that the data must live in the read-only data section of the executable.

  • "Anything declared with ::  behaves like a constant. That includes types and procs."

  • Aliases :

    Vector3 :: [3]f32
    
Variables
x: int
    // default to 0
// All below are equivalent.
x : int = 123
x :     = 123
x := 123
x := int(123)
  • Multi-declaration :

y, z: int 
    // both are int.

Literal Types

  • Literals are untyped , but untyped  values doesn't have  to be from a literal; you can get untyped  values from builtins like len  when applicable.

  • "I might say that a literal rune is a piece of syntax that yields an untyped rune".

  • untyped  usually means it comes from a literal, though sometimes intrinsics/builtins can give them too.

  • It basically just means a compile-time-known  value.

  • rgats:

    • i can see why some people prefer literals having static types, 10  is always an int in C

    • and the conversions happen at runtime

    • but i dont think it makes a very big difference in most cases

    • honestly i think it'd make a bigger difference in a language without type inference

    • in C you have to specify the type of your literal, 10 , 10u , 10f , 10l , etc, and you also have to specify the type of your variable, like unsigned long long x = 10ull;

    • c implicitly converts int  to unsigned long long  i believe, but if you actually wanted a very large number you'd need to specify the type (edited)Monday, 27 October 2025 15:31

    • so it gets extra messy there

    • and not every number converts implicitly, i dont think float x = 10.5;  works for example, which gets annoying

Untyped Types

  • Ginger Bill - Untyped Types .

  • Can be assigned to constants ( :: ) without being forced into a specific type, but once it gets assigned to a variable ( = ) it has to have an actual type.

A_CONSTANT :: 'x' 
// is an untyped thing you can make yourself

Zero Value

  • Variables declared without an explicit initial value are given their zero  value.

  • The zero value is:

    • 0  for numeric and rune types

    • false  for boolean types

    • ""  (the empty string) for strings

    • nil  for pointer, typeid, and any types.

  • The expression {}  can be used for all types to act as a zero type.

    • This is not  recommended as it is not clear and if a type has a specific zero value shown above, please prefer that.

Broadcasting

Directive
  • #no_broadcast

Example
  • Caio:

    • I have this procedure:

    tween_create :: proc(
            value:              ^$T,
            #no_broadcast end:  T,
            duration_s:         f64,
            ease:               ease.Ease = .Linear,
            start_delay_s:      f64 = 0,
            custom_data:        rawptr = nil,
            on_start:           proc(tween: ^Tween) = nil,
            on_update:          proc(tween: ^Tween) = nil,
            on_end:             proc(tween: ^Tween) = nil,
            loc :=              #caller_location
        ) -> (handle: Tween_Handle) { //etc }
    
    • And I call it with:

    tween_create(
        value = &personagem_user.arm1.pos_world,
        end = arm_relative_target_trans.pos,
        duration_s = 0.1,
        on_end = proc(tween: ^eng.Tween) {
            personagem_user.arm1.is_stepping = false
        },
    )
    
    • So why don't I get a compile error, considering that value  is a [2]f32  and end  is a f32 ?

  • Thag and Blob:

    • Because f32  can broadcast to [2]f32

    my_arr: [2]f32 
    my_arr = 3.0 
    fmt.println(my_arr) // [2]f32{3.0, 3.0}
    
    • it's really useful in certain cases

    • like allowing you to do:

    my_vec *= 2
    
    • you can add #no_broadcast param  to procs params to stop it doing so.

    • in front of the param

    #no_broadcast end: T
    
    • you can add it both to value  and end  if you want.

Casting

  • All the syntaxes below produce the exact same result.

  • Those are semantic casts. It's a compiler-known conversion  between two types in a way that semantically makes sense.

  • A straightforward example would be converting between int  and f64 ; the conversion will have the same numerical  value, which will change its representation in memory.

i := 123
f := f64(i)
u := u32(f)
i := 123
f := (f64)(i)
u := (u32)(f)
i := 123
f := cast(f64)i
u := cast(u32)f
~Auto Cast Operator
  • Auto Cast Operator .

  • The auto_cast  operator automatically casts an expression to the destination’s type if possible.

  • This operation is only recommended for prototyping and quick tests. Do not overuse it.

x: f32 = 123
y: int = auto_cast x
Advanced Idioms, Down-Cast and Up-Cast
  • union -based subtype polymorphism (Advanced Idioms) .

  • Subtyping in procedure overload :

  • Area to Hurtbox and Hurtbox to Area :

    • Very useful.

    • Caio:

      • Consider an Area  and a Hurtbox  type, where Hurtbox  inherits from Area  ( using area: Area ).

      obj := Area{
          area_entered = some_func_pointer,
          area_exited  = some_func_pointer,
      }
      fmt.printfln("OPERATION 1: %v", cast(Hurtbox)obj)
      fmt.printfln("OPERATION 2: %v", cast(^Hurtbox)&obj)
      
      • The Operation 1 is not allowed, and the Operation 2 causes a Stack-Buffer-Overflow. My question is: how / why does this happen, for both operations?

    • Barinzaya:

      • A Hurtbox  is an Area   plus more  (the Area  is just part of the Hurtbox ). When you assign obj  to be an Area , it is only  the contents of an Area , there's no extra space reserved for the extra things that a Hurtbox  would also contain.

      • Subtyping can easily downcast ( Hurtbox  to Area ) because every Hurtbox  contains a complete Area , but upcasting ( Area  to Hurtbox ) only works on an ^Area  that points into  a complete Hurtbox .

        • NOTE : You can only  cast if it's also the first field, otherwise you'd need to use container_of .

        • When you make a variable of type Area , it isn't  part of a complete Hurtbox

      • Odin doesn't implicitly embed any RTTI  (Runtime Type Information) in the type, so you can't definitively tell whether a given Area  is part of a Hurtbox  or not, so there is no dynamic_cast /type-aware pointer casting.

      • That's where patterns like union -based subtype polymorphism  come into play--that's an approach to adding  that extra information for you to know what type it is.

        • Though it stores a self-pointer, so it can cause issues if you later copy the struct without updating it.

    • Caio:

      • Isn't there a way to do something like gdscript does: if not (area is Hitbox): return , for example?

      • I mean, can I check for something like the length of the object inside the pointer, to see if the length corresponds to a complete Area or something more? I'm not sure if my question makes sense, as I don't know if checking for the content of the ^Area would give me something besides what an Area has

    • Barinzaya:

      • That would require Odin to implicitly add extra info into the struct . It doesn't do that.

      • And as for the length: That info isn't in the type. If you're talking like size_of(ptr^)  or something, the compiler is just going to give you that info based on what it knows based on the types. It doesn't do any kind of run-time lookup to try to figure it out.

      • "as I don't know if checking for the content of the ^Area would give me something besides what an Area has". That's exactly what I'm saying--there is  no other info there other than what you put in the struct . There's nothing to  check, unless you put it there yourself.

      • Subtyping is syntax sugar, and nothing more.

    • Caio:

      • So my only options are:

        1. Place some more info in the struct to avoid casting blindly

        2. Yolo cast blindly, but only do the casting if you are sure it's safe (like I'm doing for the function pointers inside the structs).

    • Barinzaya:

      • Basically, yes.

      • Number 1 is what OOP languages do, they just do it implicitly. Odin doesn't do that.

      • More specifically: that info has to come from somewhere . If all you have is an ^Area , then it has to come from inside of the struct , but it could also come from something associated with the pointer.

      • A union  of pointers or an any , they store both a pointer and  a tag/ typeid  respectively that they use to know what the pointer actually points it.

        • He means in the sense of not receiving ^Arena  directly, but an union  or any  in its place

Transmute

  • Transmute Operator .

  • It is a bitcast; that is, it reinterprets the memory for a variable without changing its actual bytes.

  • Using the same example as above, transmute ing from int  to f64  will keep the same representation in memory, which means the numerical  value will be different.

  • This can be useful for bit-twiddling things in floats, for instance; core:math  does that for some of its procs.

f: f32 = 123
u := transmute(u32)f

Type Conversions

From int  to [8]byte
  • transmute([8]byte)i

  • A fixed array is its data, so transmuting will give you the actual bytes of the int .

  • You may also want to consider casting  to one of the endian-specific integer types first if you care about the bytes being the same on big-endian systems.

From []int  to []byte
  • []int  ss a slice, but transmute ing to u8  won't change the length; a slice of 4 int s would transmute  into a slice of 4 u8 s.

  • You probably want to use slice.to_bytes  (or more generically, slice.reinterpret ). That will give you a u8  slice with the correct size.

  • The same note about endianness applies here, but it's not as straightforward to convert between the two.

From []T  to []byte
  • transmute([]byte)my_slice

    • Doesn't work well.

    • "It will literally reinterpret the slice itself as a byte slice; you have to use something in core:slice  or encoding ".

From string  to cstring
  • strings.unsafe_string_to_cstring(st)

    • Action : Alias.

    • The internal operation is:

      raw_string  := transmute(mem.Raw_String)s
      cs := cstring(raw_string.data)
      
  • strings.clone_to_cstring(s)

    • Action : Copy.

From string  to rune
  • for in

    • Assumes the string is encoded as UTF-8.

    s := "important words"
    for r in s {
        // r is type `rune`.
        // works equally for any UTF-8 char; e.g., Japanese, etc.
    }
    
    • Action : Stream

From string  to []rune
  • utf8.string_to_runes(st)

    • Action : Copy

From string  to byte
last_character := s[len(s) - 1]
    // This is a `byte` / `u8`
// string length is in bytes
for idx in 0..<len(s) {
    fmt.println(idx, s[idx])
    // 0 65
    // 1 66
    // 2 67
}
From string  to []byte
  • transmute([]byte)s

    • Action : Alias.

    • Is functionally a []byte  with different semantics, so you can transmute to it.

    • This works because their in-memory layout is the same; see runtime.Raw_Slice  and runtime.Raw_String .

    • Does not work for untyped string .

      • The type needs to be explicit.

      // Does not work
      msg :: "hello"
      data := transmute([]u8)msg
      
      // Works
      msg: string : "hello"
      data := transmute([]u8)msg
      
From string  to [^]byte
  • raw_data(s)

    • Action : Alias.

From []string  to []byte
  • It's effectively a pointer to pointers.

  • If you want the bytes of each string sequentially, you will have to loop through them and copy them into a buffer.

From cstring  to string
  • string(cs)

    • Action : Alias.

  • strings.clone_from_cstring(cs)

    • Action : Copy.

From cstring  to rune
  • .

From cstring  to []rune
  • .

From cstring  to byte
  • .

From cstring  to []byte
  • .

From cstring  to [^]byte
  • transmute([^]byte)cs

    • Action : Alias.

From []byte  to string
  • string(bs)

    • Unless it's a slice literal

    • Action : Alias.

  • transmute(string)bs

    • Action : Alias.

From []byte  to cstring
  • .

From []byte  to rune
  • .

From []byte  to []rune
  • .

From []byte  to [^]byte
  • raw_data(bs)

From byte  to string
last_character_as_byte := my_str[len(my_str) - 1]
string([]byte{ last_character_as_byte })
From byte  to cstring
  • .

From byte  to rune
  • .

From rune  to string
  • With a strings.Builder :

    • strings.write_rune

bytes, length := utf8.encode_rune(r)
string(bytes[:length])
  • utf8.encode_rune  + slice using the int  returned, to perform a string()  cast.

  • No allocation is needed.

From rune  to []byte
  • utf8.encode_rune

    • Takes a rune  and gives you a [4]u8, int  which you can slice and string cast.

From []rune  to string
From [^]byte  + length to string
  • strings.string_from_ptr(ptr, length)

    • Action : Alias.

From [^]byte  to cstring
  • cstring(ptr)

    • "C Byte Slice".

    • Action : Alias.

From struct  to [^]byte
  • cast([^]u8)&my_struct

From struct  to []byte
  • (cast([^]u8)&my_struct)[:size_of(my_struct)]

  • mem.ptr_to_bytes(ptr, len)

    • Creates a byte slice pointing to len  objects, starting from the address specified by ptr .

    • It just does transmute([]byte)Raw_Slice{ptr, len*size_of(T)}  internally.

type / typeid / size_of

Type
  • type_of(x: expr) -> type .

    • Strange.

  • Get the type of a variable :

    typeid_of(type_of(parse))
    
  • Places using expr  or type :

    • base:builtin

      type_of :: proc(x: expr) -> type ---
      
    • base:intrinsics :

      soa_struct :: proc($N: int, $T: typeid) -> type/#soa[N]T
      type_base_type :: proc($T: typeid) -> type ---
      type_core_type :: proc($T: typeid) -> type ---
      type_elem_type :: proc($T: typeid) -> type ---
      type_integer_to_unsigned :: proc($T: typeid) -> type where type_is_integer(T), !type_is_unsigned(T) ---
      type_integer_to_signed   :: proc($T: typeid) -> type where type_is_integer(T), type_is_unsigned(T) ---
      
typeid
  • typeid .

  • type_info_of($T: typeid) -> Type_Info .

  • typeid_of($T: typeid) -> typeid .

    • Strange.

  • Example :

    • Caio:

      • Why isn't this allowed?

        id: typeid = f32
        data: int = 2
        log.debugf("thing: %v", cast(id)data)
        
      • I'm trying to understand a bit more about typeid.

      • I've seen it being used as a compile time known constant in generic procedures, $T: typeid , and in this case it can be used for casting? How does this work?

    • GingerBill:

      • Because cast  is a compile time operation.

      • What you are doing requires an run time operation which is very difficult to do.

    • Barinzaya:

      • A proc argument like $T: typeid  is parapoly , which means it's basically a generic/template argument.

      • The compiler will generate a separate variation of the proc for every unique group of parapoly arguments it's called with.

      • Naturally, that means that the argument must be known at compile-time, so it can't be a variable.

    • Caio:

      • hmmm ok. So, a brief of what I was thinking of doing: I'm trying to store some data in a struct in its generic form, and then use some other data to cast it back to the original data. A any  stores exactly what I need: a rawptr  and a type, but I got confused about the typeid . Is there a way to accomplish this operation?

    • Barinzaya:

      • You basically have to just type switch on the any  and handle the cases that you care about, e.g. how fmt  handles arguments: https://github.com/odin-lang/Odin/blob/38faec757d4e4648a86fb17a1fda0e2399a3ea19/core/fmt/fmt.odin#L3168.

      base_arg := arg  // is an any.
      base_arg.id = runtime.typeid_base(base_arg.id)  // probably to avoid derivative types `my_int :: int`, something like that.
      switch a in base_arg {
      case bool:       fmt_bool(fi, a, verb)
      case b8:         fmt_bool(fi, bool(a), verb)
      case b16:        fmt_bool(fi, bool(a), verb)
      case b32:        fmt_bool(fi, bool(a), verb)
      case b64:        fmt_bool(fi, bool(a), verb)
      
      case any:        fmt_arg(fi,  a, verb)
      case rune:       fmt_rune(fi, a, verb)
      // etc
      }
      
      • A union  is usually better unless you really  need to handle anything. any  is a pointer that doesn't behave like a pointer and is easy to misuse; a union  actually contains its value. Cases needing true generic handling are rare, usually for arbitrary (de)serialization and printing.

    • Jesse:

      • any  should be avoided until all other alternatives have been explored.

      • It is almost never the case that you really don't know what set of types some data could be.

size_of
  • Why do I get a different value for size_of , between bar1  and bar2 ?

    Vertex :: struct {
        pos:   [2]f32,
        color: [3]f32,
    }
    
    foo :: proc(array: []$MEMBER) {  // passing a `[]Vertex` as a parameter
        fmt.println(size_of(MEMBER))  // prints 20
        bar1(MEMBER)
        bar2(MEMBER)
    }
    
    bar1 :: proc(member: typeid) {
        fmt.println(size_of(member)) // prints 8
    }
    
    bar2 :: proc($member: typeid) {
        fmt.println(size_of(member)) // prints 20
    }
    
    • bar1  is the typeid  of Vertex , not Vertex , so it's getting the size of a typeid .

    • typeid  is the type of types. It's a hash of the type's canonical name. At compile time the compiler knows what the underlying type is, so it'll use the type itself rather than typeid . At runtime it can't know, so it'll be a typeid .

    • Compile-time typeid s are  effectively types (which is why you can do stuff like proc ($T: typeid) -> T ), whereas run-time typeid s are indeed just an ID ( u64 -sized).