Inko 0.13.1 released

Published on

We're pleased to announce the release of Inko 0.13.1. This release includes a new C FFI, specialization of generic types and methods, and more.

While working on the 0.13.0 release we discovered an error that occurs when building Inko 0.13.0 for ARM64 platforms. This is fixed in 0.13.1, hence the version jump from 0.12.0 to 0.13.1.

Table of contents

This release includes many changes not listed in this announcement. For the full list of changes, refer to the changelog for this release.

A new C FFI and conditional compilation

Thanks to the NLnet foundation for sponsoring the development of this feature

With version 0.13.1, Inko once again has a functioning FFI for interacting with C libraries. The FFI is a compile-time FFI, instead of using e.g. libffi. We also include support for conditional compilation at the import level.

For example, to use the ceil() function from libm, you'd write the following code:

# This imports a C library.
import extern "m"

# This "attaches" a function defined in a C library.
fn extern ceil(value: Float64) -> Float64

class async Main {
  fn async main {
    ceil(1.123 as Float64) as Float

Variadic functions are also supported:

fn extern printf(format: Pointer[UInt8], ...) -> Int32

class async Main {
  fn async main {
    printf("Hello %s\n".to_pointer, "Inko".to_pointer)

C libraries imported are linked dynamically by default, but static linking can be enabled using inko build --static. The libc and libm libraries are always linked dynamically, even with the --static flag. We dynamically link by default as many Linux distributions only ship dynamic versions of libraries, requiring extra work to get the static libraries; assuming they're available in the first place. Dynamically linking some libraries while statically linking others isn't supported.

Conditional compilation is used to restrict imports to specific configurations/platforms. To import a module for 64 bits Linux platforms, you'd write the following:

import foo if linux and amd64

Conditionally compiled code in arbitrary places (e.g. in the middle of a method) isn't supported. This keeps the complexity required to make this work manageable, and forces you to push platform/configuration specific code into dedicated modules.

For more details, refer to the FFI documentation, the conditional compilation documentation, and/or commit df66fef7.

Specialization of types and methods

Thanks to the NLnet foundation for sponsoring the development of this feature

Before version 0.13.1, Inko used pointer tagging and runtime checks to handle generic types. For example, integers used pointer tagging for 63 bits integers and resort to heap allocating integers that needed the full 64 bits. References also used pointer tagging, such that generic code could (at runtime) determine if a value should be dropped, or if its reference count should be adjusted. Floats were always boxed, requiring a total of 32 bytes for a float (8 bytes for the pointer, and 24 bytes for the heap allocated value).

As of 0.13.1, the compiler generates specialized versions of generic types and methods. Int and Float are no longer boxed, there's no more pointer tagging, and no more runtime checks.

The implementation uses a slightly different approach from other compilers: instead of specializing types and methods over every type assigned to a generic type parameter, types are grouped into "shapes" and we specialize over these shapes. By grouping types together, we can provide a better balance between fast compile times and good runtime performance. Unboxed types such as Int, Float, Bool, and Nil get their own shape, and thus their own versions of types and methods. String also gets its own shape. References use two shapes, one for immutable references and one for mutable references. Owned values are all grouped into the same shape. This means that for example Array[User] and Array[Kitten] use the same specialization of the Array type.

Compared to the old runtime approach this new approach results in larger executables (though this depends on the program in question), but with better runtime performance.

See this section in the documentation and commit 3057ba7e for more details.

Private types and methods are private to their namespace

When types and methods are defined as private, they are no longer private to the module they are defined in. Instead, they are private to the namespace the surrounding module belongs to. Thus, a type Foo defined in is available to the modules,, but not to

This approach allows related modules (e.g. those provided by the standard library) to depend on shared private types and methods. In addition, it makes it possible for unit tests to test private types and methods.

See commit 81bae997 for more details.

Array is implemented in Inko

The Array type is implemented entirely in Inko, instead of acting as a wrapper around a type provided by the Rust-based runtime library. This makes maintenance easier, allows Array types to be specialized, and removes the need for runtime library calls for array operations, such as pushing a value into the array.

See commit 13e8e557 for more details.

"length" is replaced with "size"

Various types expose methods to get the number of values they store. These methods used to be called "length", but have been renamed to "size". For String methods that operate on extended grapheme clusters, the term "chars" is used to better reflect that the operation acts on grapheme clusters instead of bytes.

See commit 2771a63e for more details.

Enum is renamed to Stream

The type std.iter.Enum is now called std.iter.Stream, to not confuse users into thinking it's somehow related to enum classes created using class enum.

See commit 70728b63 for more details.

Reworked parsing and formatting of integers

Instead of using dedicated methods such as Int.from_base10 and Int.from_base16, parsing is now done using Int.parse. The format to parse is specified using a passed as a second argument:


Int.parse('123', Format.Decimal) # => 123

The same is true for formatting an Int:


123.format(Format.Decimal) # => "123"

See commit 95434442 for more details.

Arrays can be sorted

The methods Array.sort and Array.sort_by have been added. These methods are used for sorting an Array in place, provided the values stored in the Array implement the trait std.cmp.Compare:

let nums = [0, 3, 3, 5, 9, 1]

nums # => [0, 1, 3, 3, 5, 9]

The sorting algorithm used by these methods is a stable, recursive merge sort. While faster stable sorting algorithms exist (e.g. Timsort), they're much more complicated to implement compared to merge sort, and as such it's easier to implement them incorrectly. In the future we may switch to a different algorithm, if this proves necessary.

See commit 31a8a17a for more details.

Float implements the Compare trait

The Float type now implements std.cmp.Compare in accordance to the totalOrder predicate as defined in the IEEE 754 (2008 revision) specification. Per this specification, values are ordered in the following order:

  1. negative quiet NaN
  2. negative signaling NaN
  3. negative infinity
  4. negative numbers
  5. negative subnormal numbers
  6. negative zero
  7. positive zero
  8. positive subnormal numbers
  9. positive numbers
  10. positive infinity
  11. positive signaling NaN
  12. positive quiet NaN

See commit cf87e5a7 for more details.

The syntax for imports is changed

The syntax to separate modules and symbols in import statements is changed: instead of ::, you now have to use .:

# Before:
import std::foo::(A, B)

# After:
import, B)

See commit 8026b29d for more details.

Working with unique values is made a little easier

You can now use self in recover expressions, and values of type uni ref T / uni mut T (temporary borrows of a uni T value) can now be passed to arguments that expect a ref T or mut T, if the compiler can guarantee this is safe. This issue contains some more details about this change.

See commits 007495be and be0d8304 for more details.

Running inko pkg init is no longer necessary

When running package commands that alter the package manifest (inko.pkg), you no longer need to run inko pkg init to ensure the manifest file exists, as these commands now create this file automatically.

See commit 6f7a3780 for more details.

The package manager supports multiple major versions of the same package

The dependency graph of your project can now contain multiple different major versions for the same package. This means package A can depend on version 1.2.3 of the "json" package, while package B can depend on version 2.3.4 of the same package. It's not possible for the same package to depend on multiple different major versions of another package, i.e you can't depend on both version 1.2.3 and 2.3.4 of the "json" package.

This change makes dependency management less painful, as upgrading a package to a different major version no longer results in a cascade of major version updates for any package that depend on it (and so on).

See commit 256e8166 for more details.

Official support for FreeBSD

Inko now officially supports FreeBSD, and is tested against in our continuous integration setup. The minimum version we support is 13.2, though older versions may also work.

We considered also supporting OpenBSD, but decided against it. The effort required is just not worth it, given the overlap between people using OpenBSD and people interested in using Inko is likely small or even non-existing.

See commit ba7217ba for more details.

The Inko website now lists available third-party packages

While not related to the release itself, but still worth highlighting: the website now includes a page that lists available third-party packages. While the page is quite basic, it makes discovering Inko packages a little easier.

Following and supporting Inko

If Inko sounds like an interesting language, consider joining the Discord channel. You can also follow along on the /r/inko subreddit. If you'd like to support the continued development of Inko, please consider donating using GitHub Sponsors.