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
.
distinct
to introduce new types
^int
not int^
typeid
that can be used at runtime or compile timetransparent
, provide a form of inheritancerune
for a code point/characterinheritance
/polymorphism with using
go
, and whilst simple seems language support could helpinterface
mechanismThe 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.
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.
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
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.
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).
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.
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.
Not entirely buying the point about float binary literals.
In Odin literals are by default 'unsized'.
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.
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.
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.
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
parametersCan 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)
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.
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?
::
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;
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
Yes - confirmed.
In terms of the package system it appears imports can only be acyclic.
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.
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.