Beta Release 0.1.11

This release brings even more core library uniformity though some breaking changes, some syntax refinements, and an exciting experimental feature: compiler extensions!

Syntactic Cleanup

There are several areas of the language that got a bit of a facelift in this release.


Documentation strings were simplified in this release. Instead of needing to use an ugly #doc directive with a string literal, you can now use one or more /// comments before the procedure.

// Old way

#doc """
    This is a documentation string for this procedure.
f :: () { }

// New way

/// This is a documentation string
/// for another procedure, that can
/// span multiple lines easily.
g :: () { }

Default cases

Specifying the default case on a switch statement was always a little weird. It used to use the #default directive that always looked out of place, but I struggled to think of a better syntax. Now, you can simply use an underscore _.

// Old way
switch value {
    case 10 { ... }
    case 20 { ... }
    case #default { ... }

// New way

switch value {
    case 10 { ... }
    case 20 { ... }
    case _  { ... }

Piping Placeholder

Previously when using the pipe operator, the left-hand side was always placed in the first argument slot. That was sometimes a pain when the argument you wanted to pipe into was in the wrong spot.

Now you can use an underscore as a placeholder argument and the pipe operator will place the argument in that position.

double :: x => x * 2

main :: () {
    x := 10

    |> double()                       // Places in the first slot
    |> printf("Double x is {}\n", _)  // Places in the second slot

Stabilized Optional Semicolons

Optional semicolons have been an opt in feature for about two months now. I have been using them for every one of my projects and I have had no issues. For this reason, optional semicolons are now enabled by default! Having //+optional-semicolons in your code does not hurt, but it is now just a comment with no meaning.

If you have any issues with the optional semicolons, feel compelled to open a GitHub issue documenting your problem and I will work to find a solution.

Breaking Changes

In order to make the provided core library functions more cohesive, sometimes breaking changes are necessary. These changes should not affect many programs, but they are worth discussing.

Iterator uses ?T

Iterators are a core type used throughout many Onyx codebases. Internally they are stored as a data pointer for internal state, and a next procedure that takes the state and produces a value, or signals that the iterator is complete.

The next procedure used to return (T, bool). When the boolean was false, it signaled that the iterator was complete and the returned value should be ignored. This has some weird semantics, because you would have to create an empty T, even though it would never be used.

To fix this, next now returns ? T. When the value is Some(T), the iterator is not done and the value can be used. When the value is None, the iterator is done and there is no way a value can be used.

This is a breaking change because it affects the implementation of every Iterator. While this is largely constrained to the core library, any custom iterators written will need to be updated. Thankfully this change is rather easy to make.

io.Stream uses Result

In the same vein, the procedures in io.Stream were updated to use the Result type instead of relying on multiple return values in a pattern similar to Go's error handling. This change should not affect many if any Onyx programs or libraries, but it still worth noting.

case using range is no longer inclusive

This is the final breaking change, and has the potential for being quite impactful. When all things were considered, it made sense to make the breaking change now.

When you switch over an integer-like type, you can use a range in the case to specify that any of the values in the range should match. This range used to be inclusive, since it made intuitive sense when writing the following.

// Old way
s := "TeSt"
switch s[1] {
    case 'a' .. 'z' do println("Is lowercase!")
    case 'A' .. 'Z' do println("Is uppercase!")

The problem is that everywhere else in the language, a .. range was not inclusive. Since adding the inclusive-range operator last release, it makes sense to make a breaking change and change these case statements to use ..= instead.

// New way
s := "TeSt"
switch s[1] {
    case 'a' ..= 'z' do println("Is lowercase!")
    case 'A' ..= 'Z' do println("Is uppercase!")

This is slightly tricky thing to update, and is probably best handled by manually searching for all instances of switch, since there is no compiler error if .. is used. It will just be exclusive instead of inclusive.

Compiler Extensions

An experimental feature debuting this release is compiler extensions. Compiler extensions will allow user defined programs to interact with the compiler to generate auxiliary build files or supplement code generation.

The only use for compiler extensions at the moment is for procedural macros. These macros are expanded by a compiler extension that can do anything to generate the necessary code. More uses for compiler extensions will be added in the near future.

Take this theoretical example where the compiler extension generates bindings for an OpenAPI definition.

use core {*}

// Define the compiler extension by specifying the program to run.
// In this case "openapi_gen.wasm".
OpenAPIGenerator :: #compiler_extension "openapi_gen.wasm" {

// Generate a structure for FooApi that contains methods
// corresponding to the API endpoints specified by the definition.
FooApi :: OpenAPIGenerator.generate_from_url!{"http://localhost:5000/api.json"}

// The above could generate:
// FooApi :: struct {
//     foo :: (name: str) -> str { ... }
// } 

main :: () {
    // Invoke an endpoint as a function."Hello")

Read more about compiler extensions on the docs.


To update to the newest version of Onyx simply use the same install script found on the homepage. It will automatically detect your previous install and will override it with the new version.

$ sh <(curl -sSfL)

You can also run onyx self-upgrade if you are on MacOS or Linux!

Happy programming!

Full Changelog


  • Ability specify where piped arguments are placed using _.
    • x |> foo(y, _) == foo(y, x)
  • Alternative syntax for case #default .... You can now just write case _ ....
  • Alternative syntax for binding documentation using ///.
  • **Experimental** compiler extensions feature, currently used to create procedural macros.
  • core.misc.any_deep_copy
  • Ability to explicitly specify tag value for tagged unions.
    • Variant as value: type, i.e. Foo as 3: i32


  • Deprecated the use of #default in case statements. Use _ instead.
  • iter.take_one. Use instead.


There are several breaking changes in this release related to core library APIs.

  • now returns ? T instead of (T, bool)
  • io.Stream uses Result(T, Error) for return types instead of (Error, T)
  • switch over a range is no longer inclusive by default, since ..= exists now.
  • Enabled optional semicolons by default.
    • //+optional-semicolons is no longer necessary.

There are also several non-breaking changes.

  • The internal memory layout is different. See pull request #133 for details.
© 2020-2024 Brendan Hansen