logo

Odin Programming Language

Posted on: 2022-12-04

Conclusion

There is a lot to like here. It's quite powerful and relatively simple. There's nothing that leaps out as bad or ugly. I worry about some of the choices made to keep the language simple limit it too much, perhaps similarly to go.

Good

Maybe good?

Not sure

Bad

Ugly

Exclusion Summary

Discussion

The colon syntax seems to be

// name : type : value

x :: "what" // x is untyped string "what"
y : int : 20
z :: y +2

It's interesting because it doesn't need the prefix token.

x, y := 1, "hello"

The colon seems to separate the names from the rhs. : and = are separate tokens.

Can set visibility via @(private)

For loop, either requires {} or do.

for i := 0; i < 10; i += 1 { }
for i := 0; i < 10; i += 1 do single_statement()

Ifs are nice in that they can introduce variables.

Switch is nice because it allows for arbitrary expressions. It can also be used without a condition. If a switch is used by a enum, by default all values must be handled. You can use #partial to work around.

when is similar to #if in C/C++.

Break can be used with a label.

loop: for cond1 {
	for cond2 {
		break loop // leaves both loops
	}
}

Which is good in so far as it's not specifying where to jump. But the use of the : label syntax makes it seem as if it's like C/C++ goto.

Interestingly fallthrough is not underscored. It seems in general likes use of _ as in size_of.

Parameters are immutable by default. Because odin allows shadowing, you can just make a copy internally

foo :: proc(x: int) {
	x := x // explicit mutation
    ++x;
}

Odin doesn't have tuples. It can return multiple values which look a tuple.

swap :: proc(x, y: int) -> (int, int) {
	return y, x
}
a, b := swap(1, 2)
fmt.println(a, b) // 2 1

In this example we can see syntax to "structurally deconstruct", multiply return parameters.

You can name return parameters

do_math_with_naked_return :: proc(input: int) -> (x, y: int) {
	x = 2*input + 1
	y = 3*input / 5
	return
}

This is pretty cool.

Supports default values, and allows naming parameters on invoking.

Has a somewhat odd explicit overloading mechanism. Justifies this here

Variables defined without an explicit value are given a zero value.

Casting

Doesn't do any implicit conversions. Can cast with C++ style type(value). Can also use f := cast(f64)i).

Uses transmute to do a bitcast between types of the same size.

auto_cast opertaor automatically casts to the destinations type if possible.

x: f32 = 123
y: int = auto_cast x

true, false, nil.

Uses --- as undefined.

Address operator

Uses ^ for a pointer. Uses & to get a pointer. Uses ^ to dereference, but it's after.

x: ^int = nil           // Allows null ptrs
x^                      // causes a runtime panic

Dynamic Array

x: [dynamic]int
append(&x, 123)
append(&x, 4, 1, 74, 3) // append multiple values at once

Hmm. Not sure I like use of &, or that it's not a method.

Using

Used to make a type transparent. For example

Entity :: struct {
	using position: Vector3,
	orientation: quaternion128,
}

Can access positions fields directly on Entity.

This mechanism also allows subtype polymorphism.

Has or_else and or_return can be used to simplify idomatic error returning (like RETURN_ON_FAIL idiom).

Implicit Context

Seems to revolve around wanting to pass a memory allocator to 3rd party libraries. For example allocators.

It also has a logger, and the assertion failure procedure. Has a temporary allocator.

Undefined behavior

I generally agree with the point about undefined behavior. Or more specifically making an optimization based on undefined behavior seems questionable.

The point about Carbon having 0/wrap around as a choice, I'd agree it should choose one, and the most 'obvious' choice is the one that is most performance and/or detectable. The problem with returning 0 is that it is normal in lots of scenarios, and so doesn't indicate an error.

Literals

Not entirely buying the point about float binary literals.

In Odin literals are by default 'unsized'.

Sized types

I do think it does make sense to allow, different bit sizes. In particular if I want to define some representation, defining explicitly how many bits I want used is pretty useful.

Array types

I agree that the rust/carbon like syntax is a little janky around arrays

[i32; 4] 

He makes the point in Odin that you can do

a: [4]i32

var b = a[0];

It's not clear what the ordering is, he seems to be claiming a symmetry. I think it makes sense that each application is peeling off, so the order is actually reversed during access

var a : [3][2]I32;

var b := a[0];          /// Must be [2]I32
var c := a[0][0];       /// Is I32.

The ? means it's infered. Points out the Carbon use of tuple syntax is kind of weird.

a := [?]i32{i, i , i}

Has &~. It uses ~ for not. Carbon uses ^ for ~.

Odin doesn't have move semantics.

Can have distinct to define a new type MyFav, that is like a i64 but is distinct.

MyFav :: distinct i64;

Odin does not allow inferrance of the return type. His argument is not knowing what the return type is without evaluating the body. That has has issues in terms of how things are evaluated.

Odin just has for for loops. Don't require parens.

returned var

Odin doesn't need the returned var. In odin you can name the returned var. So

MakeCircle :: proc(radius: i32) -> (c: Circle) {
    c.radius = radius
    return

This is good. You can also do return c or some other variable would be ok.

Struct literals

In C we have the . prefix on struct literals, because = is an expression by default. In Odin it's not necessary as assignment is not an expression.

Go requires variables to be start capital letter to be public.

Claim is that private public is better at the package level not at the type. I guess this is because it allows opaque types.

Odin doesn't have operator overloading. The argument is you don't need it because it has

Not sure I entirely buy this. Not least because I think you would want to implement such types within a standard library. And so why not allow in user space?

He also talks about how they are moving away from LLVM. It's too big. It keeps introducing bugs.

constant parameters

Can do so via prefixing with $. That's short and sweet, but perhaps a keyword would be better.

For force_inline it uses a # prefix.

make_f32_array :: #force_inline proc($N: int, $val: f32) -> (res: [N]f32)

Can use $ to specify types

Table_Slot :: struct($Key, $Value: typeid) {
	occupied: bool,
	hash:    u32,
	key:     Key,
	value:   Value,
}
slot: Table_Slot(string, int)

Questions

Seems to have @() and #. Not clear what the difference is.

Seems # is for a tag. @ is for attributes.

It errs on allowing strings for values on attributes.

Not clear how generic parameters are set?

In C++ they are specified in <> as a separate parameters. In Odin they are in (), and this can be used for types. Perhaps the idea is that they are "all just parameters". What happens with implicit parameters? Can I currey to get a specialized implementation of a function?

Difference between :: and :=

Normal use of :: is

name : type : value

A nice thing about this, is that the type is optional. Can also name things which are not variables or have a specific type. So

value :: 10;
Int :: int;

Assignments

x: int = 123
x:     = 123 // default type for an integer literal is `int`
x := 123

It looks like :: style introduces constants

y : int : 123
z :: y + 7 // constant computations are possible

Can I define out of order?

Yes - confirmed.

In terms of the package system it appears imports can only be acyclic.

Compiler Implementation

It looks as if it's a front end, and that it uses LLVM as the IR, so as it stands it doesn't need to implement it's own IR.

It does use a somewhat interesting macro definition mechanism to define its AST types.

The compilation of the compiler is super fast it's in effect a single source file that pulls in everything else.

C-Odin

This effort is to make it so that Odin can output C and uses a kind of C IR. Since one of Odins goals is to reduce/remove undefined behavior, it means that addition actually calls a function to make sure there is wrap around behavior. Also since parameter passing evaluation order is undefined in C, but defined in Odin, it requires all parameters to a call be evaluated, potentially introducing variables before the call.

The main motivation around emitting C is around tooling and debugging.

Other links