Odin»Blog
Ginger Bill
Odin is extremely far along and pretty much nearing a conclusion as a language. However, there is still a lot to do!

The following list has not yet to been finalized:

  • Extend Foreign System
    • Support Assembly
    • Support C-Header file generation for Odin libraries
    • Support for Automatic DLL Loading

  • Custom Back End
    • To replace LLVM as the default backend (but also support LLVM too)
    • AMD64 and x86-32 support initially
    • Debugging Symbols (PDB/DWARF Generation)

  • Designing and Implementation of the Core Packages
    • Compression
    • CLI Tools
    • Mathematics and Statistics
    • Containers
    • Custom Allocators
    • OS
    • Image Reading/Writing
    • String Utilities
    • Unicode Lookups
    • Basic Networking

  • Testing Utilities
    • Compiler/Core Packages
    • User Code
    • Improved Vetting Utilities

  • Documentation
  • Tutorials
  • Finalize Syntax


Contributions to the language are always welcome! There is a lot to work to do and

Merry Christmas and have a Happy New Year!


- Bill
Ginger Bill
Original Comments:
https://github.com/odin-lang/Odin/issues/256#issuecomment-418073701
https://github.com/odin-lang/Odin/issues/256#issuecomment-418289626


There will never be software exceptions in the traditional sense. I hate the entire philosophy behind the concept.

Go does have exceptions with the defer, panic, recover approach. They are weird on purpose. Odin could have something similar for exceptional cases.

You can the exact same semantics as a try except block by using a switch in statement. The same is true in Go. The difference is that the stack does not need to be unwinded and it's structural control flow.

Odin has discriminated unions, enums, bit sets, distinct type definitions, any type, and more. Odin also have multiple return values. Use the type system to your advantage.

I do hate how most languages handle "errors". Treat errors like any other piece of code. Handle errors there and then and don't pass them up the stack. You make your mess; you clean it.

---------

To expand on what I mean by this statement:

You can the exact same semantics as a try except block by using a switch in statement.


Python:
1
2
3
4
5
6
7
8
try:
	x = foo()
except ValueError as e:
	pass # Handle error
except BarError as e:
	pass # Handle error
except (BazError, PlopError) as e:
	pass # Handle errors


Odin:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Error :: union {
	ValueError,
	BarError,
	BazError,
	PlopError,
}

foo :: proc() -> (Value_Type, Error) { ... }

x, err := foo();
switch e in err {
case ValueError:
	// Handle error
case BarError:
	// Handle error
case BazError, PlopError:
	// Handle errors
}


The semantics are very similar in this case however the control flow is completely different. In the exceptions case (shown with Python), you enclose a block of code and catch any exceptions that have been raised. In the return value case (shown with Odin), you test the return value explicitly from the call.
Exceptions require unwinding the stack; this is much slower when an exception happens compared to the fixed small cost of a return value.

In both cases, a "catch all" is possible:
Python
1
2
3
4
try:
	x = foo()
except Exception:
	pass # An error has happened

Odin:
1
2
3
4
x, err := foo();
if err != nil {
	// An error has happened
}


One "advantage" many people like with exceptions is the ability to catch any error from a block of code:
1
2
3
4
5
6
try:
	x = foo()
	y = bar(x)
	z = baz(y)
except SomeError as e:
	pass


I personally see this as a huge vice, rather than a virtue. From reading the code, you cannot know where the error comes from. Return values are explicit about this and you know exactly what and where has caused the error.

One of the consequences of exceptions is that errors can be raised anywhere and caught anywhere. This means that the culture of pass the error up the stack for "someone else" to handle. I hate this culture and I do not want to encourage it at the language level. Handle errors there and then and don't pass them up the stack. You make your mess; you clean it.

Go's built-in `error` type has the exact same tendency of people return errors up the stack:
1
2
3
if err != nil {
	return nil, err
}


From what I have read, most people's complaints about the Go error handling system is the if err != nil, and not the return nil, err aspect. Another complain people have is that this idiom is repeated a lot, that the Go team think it is necessary to add a construct to the language reduce typing in the draft Go 2 proposal.

-----------------

I hope this has cleared up a lot of the questions regarding Odin's take on error handling. I think error handling ought to be treated like any other piece of code.


With many rules, there will be unexpected emergent behaviour.


P.S. If you really want "exceptions", you can `longjmp` until the cows come home.

Ginger Bill
n.b. This is a philosophical article and not a technical article. There are no correct answers to the questions that I will pose -- only compromises.

I'm considering what the "best" declaration syntax would be. Historically, there have been two categories: which I will call qualifier-focused and type-focused.
An example of qualifier-focused would be the Pascal family. An example of type-focused would be the C family. Odin, like Jai, have been experimenting with an name-focused declaration syntax. These categories place emphasis on different aspects of the declarations.

  • Qualifier-focused places emphasis on the kind/qualifier of declaration (`var x = 123; const K = true;`)
  • Type-focused places emphasis on the type of the declaration (`int x = 123; bool const K = true;`)
  • Name-focused places emphasis on the name of the declarations and that the right hand side must be an expression (`x := 123; K :: true;`)

Some languages have a hybrid approach to the syntax of declarations. Python uses a form of name-focused for general variable declaration but qualifier-focused for function, class, and import declarations. Most modern derivatives of the C family may use type-focused for most declarations but use qualifier-focused for import declarations.

There are some issue regarding all three approaches.

Qualifier-Focused

  • Most forms of qualifier-focused require numerous keywords to specify the kind of declaration
  • Qualifier-focused adds verbosity to the syntax due to the extra keywords
  • Qualifier-focused declarations have a tendency to make you define all declarations at the top of a scope and/or group declarations together of the same kind together. This nudges the programmers to not intermingle declarations and assignments. Depending on the views of programmer, this can be viewed as a positive aspect.

  • Type-Focused

    • Every declaration must be associated with a type and thus be part of the type system. This is one of the reasons why `void` is in C. If a function must specify the return type, the solution to this is to create a "non-type", i.e. `void`. Having a `void` type does cause issues in the type system in general but I will not consider those in this article.
    • In the case of C++ and others, type inference must be done through a form of qualifier-focused (e.g. `auto` or `var`)

    Name-Focused

    • Name-focused has three aspects to the declaration, the left hand side (lhs), the right hand side (rhs), and the middle part. The lhs aspect are the names of the entities to be declared whilst the rhs aspect of the declaration are forms of expressions. This means that all declarations must be a declaration of an expression. This does mean that all declarations must be assigned with a form of expression. The middle part denotes the type which could be optional.
    • In a self-contained language, having only expression assigned declarations is not a problem. The difficulty comes from interfacing with foreign code, such as C, and having a consistent syntax. In C, there are two forms of function declarations: function prototypes and full functions with bodies. A function prototype is not a form of expression. A function with a body which can be thought of a named lambda function.
    • An inconsistency with name-focused is with variable declarations without an assignment (`x: int;`). It is implied that the declaration has an implicit rhs expression, the zero/default value. It is also implied that the declaration must be a variable declaration (and not a constant declaration).

    On the Aesthetics of Qualifier-Focused

    I will be open, I have a minor bias towards qualifier-focused due to Pascal being one of my very first languages. So when I started creating Odin, the language that I am designing and making, I started with a very Pascal syntax (including `begin` and `end`) but when the language became "public", the syntax changed to be closer to that of Jonathan Blow's language, Jai. I was intrigued by the idea of the name-focused syntax with its very elegant approach to type inference. However, I have had doubts about the syntax for quite a while. At one point, I struggled to find a solution to the issue of foreign procedures and foreign variables. Originally, I solved the issue for foreign procedures with replacing the procedure body with a `#foreign` tag. However, this approach cannot be applied to a foreign variable declaration and still be consistent. On an impulsive whim, I switched the entire declaration syntax to a Go-like qualifier-focused style for 2 weeks. (I have done this switch twice.) The solution to the foreign entities was to have a procedure lambda without a body by replacing the body with `---` and surrounding all foreign declarations in a `foreign` block. In Odin, a procedure without a body cannot be distinguished from a procedure type and thus there needed to be a way to specify a procedure literal/lambda without a body.

    1
    2
    3
    4
    foreign my_lib {
        some_var: i32;
        amazing_foo :: proc "c" (a, b: i32, c: f32) -> rawptr ---;
    }
    


    There are two reasons for this conflict that I have between qualifier-focused and name-focused. The first is that name-focused is elegant and terse to write compared to qualifier-focused. The second is the "ugliness" of qualifier-focused with conjunction with other forms of statements.

    Qualifier-focused looks "ugly" when it is combined with control statements:
    1
    2
    for var x = 0; x < 10; x += 1 {
    }
    

    The two keywords together in the `for var` block looks "dense and wrong" to me and makes reading the construct much more difficult. However, placing an open parenthesis in between the keywords reduces some of this "density":

    1
    2
    for (var x = 0; x < 10; x += 1) {
    }
    


    It does look slightly better but it is still "dense". These parentheses make it less "ugly" for some reason and it's not self apparent as to why the separation between the two words by punctuation improves matters.

    This is probably a reason as to why Go uses the `:=` operator, especially in this case:
    1
    2
    3
    4
    for x := 0; x < 10; x += 1 {
    }
    for idx, val := range array {
    }
    


    `:=` is a pragmatic solution to this aesthetic problem with the qualifier-focused `var`. However, this is not to say that the `:=` operator is great. In Go, it has extra semantics to make Go feel more like a "dynamic language" (variables will be shadowed with `:=`). Even if you had both `var` and `:=`, and that they did the exact same thing, it does beg the question: why have two things that do exactly the same thing?

    I have been researching the topic of syntax in language design for a long time now. It's been an interesting topic and I think I should actually write most of my findings. I hope this condensed explanation of the issues with regards to declaration syntax has aided others as to my predicament.

    - Bill
Ginger Bill
Hello everyone!

Odin is been through many changes and improvements lately, many of which I have not announced at all.

Below is an overview of the new upcoming features in the next release of Odin:

  • Decent import system
    - `import`, `using import`, `export`
    - Library Collections
  • Foreign library system
    - `foreign`, `foreign export`, `foreign import`
  • File scope `when` statements
  • Attributes
  • Syntax updates
    - `switch`
    - `inline proc "c" (x: string) {}`
  • Array Programming
  • `uintptr`
  • Polymorphic value parameters
    - `[$N]$T`

Import System

For v0.7, the main feature I have been working on is the import system. I wanted a system that was:
  • Simple to use
  • Make it possible to "think locally" about a problem
  • Allowing for a form of cyclic importation
  • Be able to store a library across multiple files
  • Allowing for configuration with file-scoped `when` statements (compile-time `if` statements)

After a few months of design and programming, I have finally got a model that I am happy with (for the time being).

To solve these criteria, Odin has three different forms for importation declarations, `import`, `export`, and `using import`.

`import` is the standard approach to import a file. This declaration will create an import name entity which acts as an alias to the imported file's scope. This is the preferred approach to importing files as it creates an import name, making it clear to the reader where something is defined. The `import` statement will try to take the name of the file as the import name if a custom name is not provided.

1
2
import "foo.odin"                        // creates an import name `foo`
import lnfaf "long_name_for_a_file.odin" // creates an import name `lnfaf`


The second method is `using import`. As the name and the semantics of `using` suggest, it imports the contents of that file's scope into this file. However, the entities (a named declaration such as a variable, constant, procedure, etc) that were imported, are local to this file only, which means that on subsequent importations of this file, those entities will not be seen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// a.odin

FOO :: 123;

// b.odin
using import "a.odin"

// c.odin
import "b.odin"

/*b.FOO not exported by `b.odin`*/


The third and final method is `export`. This is similar to `using import` but with one important difference: import entities are also exported on subsequent importations. This can be thought of as a fancy "`#include`". Its main use is to allow the ability to store a library across multiple files and then treat the subsequent file as the library.

1
2
3
4
5
// foo.odin
export "foo_part1.odin"
export "foo_part2.odin"
export "foo_part3.odin"
export "foo_part4.odin"


One of the apparent issues with this system is the natural possibility of cyclic importations. Due to a lot of other issues and requirements, what I have settled on is only allowing cyclic `import`s and disallowing cycles with `using import` and `export`. This will be made a little more clearer very soon.

Odin has a lack of forward declarations; this requires that the entities are to be evaluated out of order (at the file scope). This would not be much of a problem if I didn't require support for more than one platform, in a sane manner. Configuration for certain platforms is to be achieved with a `when` statement at the file scope.

1
2
3
4
5
6
7
when ODIN_OS == "windows" {
    import bar "bar_windows.odin"
} else when ODIN_OS == "osx" {
    import bar "bar_osx.odin"
} else {
    import bar "bar_nix.odin"
}


Allowing `when` statements causes two evaluation issues: the order of evaluation for files, and the order of evaluation for file-scoped `when` statements.

To solve the first issue, the priority ordering of the files can be determined by pretending if the `when` statements don't exist and determine the load order for all possible configurations.

To solve the second issue, the entity evaluation is done as an iterative process:
  • Collect all entities in all files
  • Evaluate `when` statements in source order and in file priority/load order
  • Collect all entities within the `when` statement
  • Evaluate nested `when` statements
  • Rinse and repeat

The reasoning for the in-order evaluation for `when` statements is to simplify the compiler and reduce compilation speeds by not doing a full import graph analysis (which is very difficult to solve).

Side note: There were plans originally to abstract the concept of a file and to have the concept of a "package" however, for the time being, the "file is a scope" concept is good enough for most problems. The "package" idea was to aid with organization and versioning but this be moved to a future release.

Library Collections

One issue regarding libraries are search paths. Previously, if a file could not be found relative to the current file, it would search in the "core" directory for that file. This approach has some issues:

  • It is not clear to the user that this would be case.
  • What if the user has a "local" file of the same name as the "core" one and thus the "local" file overrides the "core" file?
  • What if the user wants a custom search path?

To solve these issues, I added a simple concept of a library collection. A collection is a name that refers to search path. If a collection is not provided in the importation statement, then the file is to be searched relative to the current file.

1
2
3
import "core:fmt.odin" // search in `core` collection
import "shared:glfw.odin" // search in `shared` collection
import "local.odin" // search relative to current file


It is possible to add new library collections with `-collection=foo=path/to/thing`.

Foreign System and Attributes

When using a new language such as Odin, it is useful and needed to interface with "foreign" languages and libraries. To ease this process, Odin has the concept of `foreign`.

To use a foreign library, it must be imported using `foreign import`.

1
2
3
foreign import stbi "stb_image.lib" // search relative to current file
foreign import glfw "system:glfw3dll.lib" // search in the system path
foreign import "system:kernel32.lib" // use the file name as the "foreign import name"


In order to use a foreign library, entities must be associated with it. This is done with a `foreign` block.

1
2
3
4
5
6
7
8
9
foreign import glfw "system:glfw3dll.lib"

foreign glfw {
		glfwInit       :: proc "c" () -> i32 ---;
		glfwTerminate  :: proc "c" () ---;
		glfwWindowHint :: proc "c" (hint, value: i32) ---;

		// etc
}


n.b. The `---` signifies to the compiler that this is a procedure literal that has its body defined elsewhere. This is needed because the only thing that distinguishes a procedure type/signature from a procedure literal is its body.

Within a `foreign` block, only procedure declarations and variable declarations can be defined as these are typically the only entity kinds that are exported from a foreign library. If an entity within a foreign block is not used within the program, the library does not get linked, thus reducing the dependencies for the application.

Attributes

When using a `foreign` library, it is useful to rename the entities whilst keeping the underlying link name intact. This can be achieved using the new attribute system.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
foreign gb {
		@(link_name="gb_foo")
		foo :: proc "stdcall" (x: i32) -> bool ---;
		
		@(link_prefix="gb_")
		bar: i32; // foreign variable
}

@(default_calling_convention="c", link_prefix="glfw")
foreign glfw {
		Init       :: proc() -> i32 ---;
		Terminate  :: proc() ---;
		WindowHint :: proc(hint, value: i32) ---;
		// etc
}


Attributes can be applied to any procedure or variable declaration at the file scope, regardless whether or not they are in a `foreign` block.

1
2
3
4
5
6
7
8
@(thread_local)
some_var: i32;

@(thread_local="initialexec") // custom thread local model
other_var: f32;

@(link_name="my_foo_bar")
foo_bar: i32;


Foreign export

Instead of compiling to an executable, it is sometimes useful to compile a program to a library itself. This is achieved with using `odin build_dll`. However, when compiling a library, you need to signify what entities will be exported. This can be achieved using `foreign export` as either a block or a prefix.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
foreign export {
	@(link_name="gb_foo")
	random_number :: proc "c" () -> i32 {
		return 4;
	}
	
	bar: i32;
}

foreign export 
my_func :: proc "c" (x: i32) -> bool {
	return x == 123;
}


Array Programming

Odin has had the concept of a `vector` type of quite a while now. It is meant to map directly to SIMD vector instructions rather than be used as a mathematical vector type. However, due to its sanity features of the operators it allows, it has been abused.

This is why Odin now supports array programming for fixed sized arrays.

1
2
3
4
a := [2]i32{1, 2};
b := [2]i32{7, 4};
c := (-a + b) / 2;
fmt.println(c); // [3, 1]


Arrays (`[N]T`) and vectors (`[vector N]T`) have slightly different semantics and because of this, they have completely different use cases.

  • A vector has certain alignment requirements which allows it to map to SIMD instructions whilst an array has normal alignment rules (alignment of the element).
  • An array can allow for arrays of arrays whilst a vector can only store an integer, float, or boolean.

Polymorphic value parameters

In order to take advantage of the new array features, I wanted to make creating polymorphic procedures for arrays easier, especially for array count parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dot :: proc(a, b: $T/[$N]$E) -> E {
	res: E;
	for i in 0..N do res += a[i] * b[i];
	return res;
}

cross :: proc(x, y: $T/[3]$E) -> T {
	a := swizzle(x, 1, 2, 0) * swizzle(y, 2, 0, 1);
	b := swizzle(x, 2, 0, 1) * swizzle(y, 1, 2, 0);
	return T(a - b);
}


n.b. The polymorphic array count cannot be an complex expression as this would require an algebraic solver. So it can only be a simple `$N` value.

Syntax Changes and Other Improvements

  • `match` has been replaced with `switch`
  • `uintptr` which can be casted to and from any pointer. It signifies to the user that this is an integer than can hold any pointer.
  • `inline` and `no_inline` are keywords and are used as prefixes for procedure literals
    - `inline proc() -> i32 { ... }`
  • Procedure calling conventions are set by writing a string literal with the calling convention just after the `proc` keyword
    - `proc "c" (x: i32)`
  • `foreign_library` and `foreign_system_library` have been replaced with `foreign import`

Looking to the Future

I have created a general plan for Odin and what to expect in the coming future:
  • v0.7 - Import system (coming soon)
  • v0.8 - Debugging Symbols
  • v0.9 - Custom Backend
  • v0.10 - Compile Time Execution
  • v0.11 - Core Library Improvements
  • v0.12 - ???

Along side the compiler and the language, there will be some other side projects to accompany Odin.

  • A code execution playground which allows users to execute Odin (and other languages) directly from the web browser without the need to download the compiler.
  • Improvements to the Odin Website
  • Documentation for the Odin Language and compiler so that others can learn

Thank you everyone for your support, bug issues, suggestions, testing, and creations in Odin.

Regards,

Bill
Ginger Bill
I've just started a Patreon page for Odin to aid with development (and beer money).

https://www.patreon.com/gingerbill

Odin will still remain free and open for everyone so please don't feel forced to donate if you don't want to.

Thank you very much for everyone's support so far and let's make Odin great for everyone!