Designing Odin’s Casting Syntax
Series: Syntax and how it Matters
2026-02-23
Odin’s declaration syntax becomes second nature to everyone who uses the language but I do sometimes get asked “Why are there two ways to do type conversions?” Enough that I had to make an FAQ entry..
cast(type)value
type(value) or (type)(value)
The reason that there are two ways to do type conversions is because one approach may feel better than the other case. If you are converting a large expression, it sometimes a lot easier to use the operator-style approach, cast(type). The call syntax is commonly used to specify a type of an expression which may be relatively short such as u32(123) or uintptr(ptr).
There are two other type conversion operators, transmute and auto_cast.
The general design of this took a lot of trial and error in the early days with people giving me feedback about what felt right and wrong.
Departing From C’s Syntax
In C, type conversions use the following syntax (type)value. The problem with this syntax is that it requires a context-sensitive grammar to determine whether the expression of type is actually a type whilst parsing. Since Odin is designed to be a context-free grammar, this syntax is not possible. There are a few reasons this is not possible:
- Odin’s idea that
(x) == x, and the parentheses are just for grouping expressions, and has no semantic meaning. - Ambiguity in contexts like
(a)+b- Is that a type cast or a binary expression?
- Not necessarily that obvious when scanning code.
As I discuss in the previous article, sometimes it is good to keep to familiar syntax but if the parsing is awful or the underlying semantics wants the syntax to be something else, then departing from the familiar is usually the way to go. Be coherent with the language you are designing, not with some other language that is not this one.
The Syntax Ideas
Verbosity is actually a problem when you realize how much noise casting produces when you have to do it a lot, especially in a language like Odin with distinct typing (i.e. there is very little implicit type conversions, even between integers, meaning you need to do explicit casts).
I went through a plethora of different syntax for Odin’s type casting:
x as T
x.(T) // now used for type assertions
cast(x, T)
cast(T, x)
cast(T)x // used
T(x) // used
(T)(x) // used
// and a few more but they were just too bad to mention
One thing to consider is the need for parentheses and how that can actually cause issues in terms of scannability and ergonomics (not typing but flow). The cast(T, x) like syntaxes actually required more parentheses in practice that you might realize.
Reflecting on the Semantics of Declarations
The flow aspect was actually interesting because I wanted the syntax to match the semantics more correctly and I found that the type must be on the left of the expression since that is how declarations work too: name: type = value, so a cast would make sense that way too: name := type(value). This also turned out to be a similar realization in languages like Newsqueak (where that declaration syntax originates from) and Ada. This actually ruled out a lot of the other syntax options as a result.
But before that, I experimenting with x as T because it seemed like a good idea but turned out to be a mess because of precedence rules. Either as was “tight” towards the expression meaning you then had to use a lot of parentheses to be clear what was being cast, or you had it very “loose” towards the expression which lead to the same problem. as didn’t reduce the need for parentheses in practice and only led to confusion with precedence.
I then reused x.(T) syntax for the type assertions. One because it has some familiarity from Go but also because the “type” itself is the tag in the union, making it feel more like a field access using a type. The parentheses around the type in this case are necessary to remove any ambiguity syntactically and semantically.
Optimizing for the Common Use Case
This then lead to the possibilities of T(x), cast(T)x and cast(T, x). Odin’s type system is a bit different to other languages so sometimes people don’t always realize the consequences. A good example of this is with the constant value system. 123 is an “untyped” number (existential typing if we are being accurate, but that confuses people so I stuck to the terminology of “untyped”), and you sometimes want this to be a specific type. Many languages “solve” this by having suffixes on literals e.g. 123i32
Odin also supports imaginary numbers so that could have been tokenized as 123i thus being a little ambiguous., but this is not an option in Odin because of distinct typing allows anyone to create their own distinct form of a type
For more information on distinct types, check the overview: https://odin-lang.org/docs/overview/#distinct-types. So if I wanted to keep that syntax short for the most common use case of casting, T(x) was unironically the best option possible.
For when a prefix style of casting was desired, doing cast(T, x) wasn’t really aiding in reading any more than cast(T)x. I also didn’t want then to be built-in procedures because that actually means they would not be keywords but identifiers, since even i32 in Odin is an identifier
I did not want to have something as common and as important as casting operations be mere builtin procedures like len or size_of. and not a keyword. So if I wanted them to be a features of the languages using keywords, making them procedure calls felt very wrong. It might not be the most “robust” of justifications but that is because it is fundamentally about designing for humans and what they like, not some preconceived notion of “consistency”.
Scannability is Very Important
As I say in the previous article, I will stick to coherency over consistency if necessary, and this was on of those cases. And I didn’t want to fall into the trap that some languages have done which makes the entire thing unscannable. Zig is a “great” example of a language with poor scannability due to its very dense and sigil-heavy syntax. And it having way too many casting operations (17+ IIRC) does not help this matter any better, nor does not actually give any real benefit in the long run to anyone (even the compiler). A real world example of this syntactic mess I am not sure what to call this but it’s the exact opposute of syntactic diabetes.:
const gap: f32 = @divTrunc(@as(f32, @floatFromInt(rl.getScreenWidth() - (4 * objectWidth))), 5.0);
const offsetX: f32 = @as(f32, @floatFromInt(index + 1)) * gap + @as(f32, @floatFromInt(index)) * @as(f32, @floatFromInt(objectWidth));
The equivalent in Odin would be written as this:
gap := math.trunc(f32(rl.GetScreenWidth() - 4 * objectWidth) / 5)
offsetX := f32(index+1) * gap + f32(index)*f32(objectWidth)
Note that where many of the parentheses exist in that Odin snippet, most would already exist any way without the explicit casting, and thus all you are doing is annotating the grouped expressions with a specific type.
Design as a Human Endeavour
All I can say is, humans are odd creatures and you’ll be surprised how they think in practice. Design is about understanding humans. How they function, mentally and physically. Their perception, psychology, sociology, physiology, ergonomics, needs, desires, etc. It’s all about being able to put yourself in other people’s shoes, more than making the thing.
Designing a programming language is no exception to this. Syntax is how we express the semantics we want in a program. It’s the interface to the code itself. The littlest of things matter and they do add up when you have so many little things.