Odin Changes, Improvements, and the Future

Ginger Bill  —  11 months, 2 weeks ago [Edited 32 minutes later]
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
Log in to comment