Declaration
Constants
u :: "what";
// Untyped.
y : int : 123
// Explicitly typed constant.
-
::is closer to#definethan it isstatic 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, butuntypedvalues doesn't have to be from a literal; you can getuntypedvalues from builtins likelenwhen applicable. -
"I might say that a literal rune is a piece of syntax that yields an untyped rune".
-
untypedusually 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,
10is 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, likeunsigned long long x = 10ull; -
c implicitly converts
inttounsigned long longi 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
-
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:
-
0for numeric and rune types -
falsefor boolean types -
""(the empty string) for strings -
nilfor 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
valueis a[2]f32andendis af32?
-
-
Thag and Blob:
-
Because
f32can 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 paramto procs params to stop it doing so. -
in front of the param
#no_broadcast end: T-
you can add it both to
valueandendif 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
intandf64; 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
-
The
auto_castoperator 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
-
Subtyping in procedure overload :
-
Area to Hurtbox and Hurtbox to Area :
-
Very useful.
-
Caio:
-
Consider an
Areaand aHurtboxtype, whereHurtboxinherits fromArea(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
Hurtboxis anAreaplus more (theAreais just part of theHurtbox). When you assignobjto be anArea, it is only the contents of anArea, there's no extra space reserved for the extra things that aHurtboxwould also contain. -
Subtyping can easily downcast (
HurtboxtoArea) because everyHurtboxcontains a completeArea, but upcasting (AreatoHurtbox) only works on an^Areathat points into a completeHurtbox.-
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 completeHurtbox
-
-
Odin doesn't implicitly embed any RTTI (Runtime Type Information) in the type, so you can't definitively tell whether a given
Areais part of aHurtboxor not, so there is nodynamic_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:
-
Place some more info in the struct to avoid casting blindly
-
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 thestruct, but it could also come from something associated with the pointer. -
A
unionof pointers or anany, they store both a pointer and a tag/typeidrespectively that they use to know what the pointer actually points it.-
He means in the sense of not receiving
^Arenadirectly, but anunionoranyin its place
-
-
-
Transmute
-
It is a bitcast; that is, it reinterprets the memory for a variable without changing its actual bytes.
-
Using the same example as above,
transmuteing frominttof64will 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:mathdoes 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
-
[]intss a slice, buttransmuteing tou8won't change the length; a slice of 4ints wouldtransmuteinto a slice of 4u8s. -
You probably want to use
slice.to_bytes(or more generically,slice.reinterpret). That will give you au8slice 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:sliceorencoding".
-
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
[]bytewith different semantics, so you can transmute to it. -
This works because their in-memory layout is the same; see
runtime.Raw_Sliceandruntime.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 theintreturned, to perform astring()cast. -
No allocation is needed.
From
rune
to
[]byte
-
utf8.encode_rune-
Takes a
runeand gives you a[4]u8, intwhich you can slice and string cast.
-
From
[]rune
to
string
-
-
"C Byte Slice".
-
Action : Copy.
-
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
lenobjects, starting from the address specified byptr. -
It just does
transmute([]byte)Raw_Slice{ptr, len*size_of(T)}internally.
-
type / typeid / size_of
Type
-
-
Strange.
-
-
Get the type of a variable :
typeid_of(type_of(parse)) -
Places using
exprortype:-
base:builtintype_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. -
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
castis 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: typeidis 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
anystores exactly what I need: arawptrand a type, but I got confused about thetypeid. Is there a way to accomplish this operation?
-
-
Barinzaya:
-
You basically have to just type switch on the
anyand handle the cases that you care about, e.g. howfmthandles 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
unionis usually better unless you really need to handle anything.anyis a pointer that doesn't behave like a pointer and is easy to misuse; aunionactually contains its value. Cases needing true generic handling are rare, usually for arbitrary (de)serialization and printing.
-
-
Jesse:
-
anyshould 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, betweenbar1andbar2?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 }-
bar1is thetypeidofVertex, notVertex, so it's getting the size of atypeid. -
typeidis 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 thantypeid. At runtime it can't know, so it'll be atypeid. -
Compile-time
typeids are effectively types (which is why you can do stuff likeproc ($T: typeid) -> T), whereas run-timetypeids are indeed just an ID (u64-sized).
-