backgroundradial

Onyx, a new programming language powered by WebAssembly

Learn about Onyx, a new imperative programming language that leverages WebAssembly and Wasmer for seamless cross-platform support

brendanfh avatar
brendanfh
Brendan Hansen

Guest Author

wasmer

November 30, 2023

arrowBack to articles
Post cover image

What is Onyx?

Onyx is a new programming language featuring a modern, expressive syntax, strict type safety, blazingly-fast build times, and out-of-the-box cross platform support thanks to WebAssembly.

Over the past 3 years of development, Onyx has become a complete programming language with features every developer needs: a fast compiler, a package manager, a language server, editor support, and cross-platform deployment using Wasmer and WASIX.

Onyx’s syntax is inspired by other imperative programming languages, namely Go, Jai and Odin. With Onyx however, it is possible to program in a more functional style through some small language features, such as the pipe operator (|>).

Take a look at this program written in Onyx that prints the sum of some numbers!

// Include libraries from the standard library 
use core {*}

// All programs start in main.
main :: () {
    input := "111 110 121 120";

    // Chain the output of one function
    // to the input of another!
    sum := string.split_iter(input, " ")
        |> iter.map(x => cast(i32) conv.parse_int(x))
        |> iter.fold(0, (a, b) => a + b);

    printf("Sum of input: {}\n", sum);
}

What can Onyx do?

Onyx can make everything from HTTP servers to video games. I have used it to make games, web applications, and even a slide deck presenter. I am excited for more users to expand Onyx’s use cases. Take a look at these guides on the Onyx website to get started using Onyx for your next project!

Why WebAssembly?

When making a programming language, you need to decide how your programs will actually be executed. Will they be interpreted like Python or JS? Compiled to native code like Rust or C++? Or somewhere in between, like Kotlin or Elixir?

I knew I wanted Onyx to compile to WASM, because of cross-platform support and the simplicity of generating a valid WASM module. Onyx does not rely on libraries like LLVM to generate its code, because WASM is so simple to target.

To give a brief introduction for those unfamiliar, WebAssembly instructions are executed on a stack-based virtual machine. This means all operations are done by pushing and popping operands on a stack. When it’s time to run your code, these stack operations are then translated to native instructions to run directly your CPU.

For example, to add two numbers, say 37 and 42, we would use the following WASM instructions.

i32.const 37
i32.const 42
i32.add

The first two instructions push the integers 37 and 42 onto the stack. The final instruction pops two integers off of the stack (37 and 42) and pushes the resulting integer (79) back onto the stack. This method of computation gives us a simple way to make the expression emitter in a compiler.

In a compiler, the source code goes through a series of steps to produce the final executable code.

Parsing  ->   Semantic Checking     ->  Code Generation
(Tokens)    (Abstract Syntax Trees)      (Target code)

This is a simplified view, but it gives us the right idea. The key is to look at which data structures are used in each phase of the compiler. In the parsing phase, we are looking at individual tokens, things like 123, variable_names, and symbols. The parser then generates a tree data-structure that represents our program in a manageable way. After the checker ensures our program makes sense and nothing is wrong, the code generator translates the syntax trees into the target assembly or byte code.

The code generator step is where WASM shines. Because our input to the code generator is (generally) a syntax tree, we simply do a post-order traversal of the tree and we have the instructions in the right order.

Let's look at this example. The expression 3 * 4 + 5 would result in a tree with the post-order traversal of 3 4 * 5 +. When we convert that into WASM instructions, we get the following code.

i32.const 3
i32.const 4
i32.mul
i32.const 5
i32.add

You can double check by hand, but the resulting value is 17, or 3 * 4 + 5.

Here is some Onyx pseudo-code to show how you could easily implement this.

generate_code :: (n: &Node) {
    switch n {
        case binary_op: .Binary_Operation {
            // Emit the expression on the left hand side.
            generate_code(binary_op.left);

            // Emit the expression on the right hand side.
            generate_code(binary_op.right);

            // Emit the actual operation.
            generate_operation_for_binary_op(binary_op.operation);
        }

        case integer: .Integer_Literal {
            generate_code_for_integer_literal(integer);
        }

        // ...
    }
}

We generate all expressions using a recursive function that first visits the left-hand side, then the right-hand side, then generates the operation's corresponding instruction.

How does Onyx use WebAssembly?

When creating Onyx, I wanted to push the limits of WebAssembly. WASM is an awesome technology that is a great compilation target, but there are some inherit drawbacks. The major drawback is the impossibility of implementing a graphical interface or a game outside of the browser. This is by design, as WASM is entirely isolated from its host environment. To navigate these limitations I came up with a way for any WebAssembly embedder, like Wasmer, to interact with native system components.

My strategy was to wrap libwasmer.a, the standalone library version of Wasmer, into my own custom WASM loader, to allow imported functions to be linked against native libraries. I added a custom section to the WASM binaries output by Onyx that specifies which native libraries it wants to link against. When linking, I find the libraries on disk, load them, call a function in each dynamic library that returns a list of procedures that can be used when linking. This external linking enables Onyx to use any native C library, from Raylib and OpenGL, to PostgresQL and OpenSSL.

How does Wasmer fit in?

Wasmer has been crucial to Onyx's development from the very beginning.

In the early stages of the language, the Wasmer CLI was used to test the binaries produced by Onyx and to ensure everything was working properly.

Now, Wasmer is the underlying runtime used in most distributions of Onyx, because it enables fast, cross-platform support, allowing Onyx to be used anywhere.

Thanks to Wasmer Edge, the Onyx website, onyxlang.io, is no longer hosted on my personal VPS.

Conclusion

If Onyx sounds interesting to you, you can learn more at onyxlang.io. You can find installation instructions, getting started guides and example projects to get yourself going. As of November 2023, Onyx is still very much in a beta state, but the only way for it to get out of beta status is for more people to use and stress test it! I'm always open for feedback and pull requests!

Thanks for taking the time to learn about Onyx. I can't wait to see what you build with it!

About the Author

Brendan Hansen is an experienced software engineer working as a researcher, focused on cyber security and machine learning. Outside of work, he enjoys continuing to push his development skills by working on personal projects, like Onyx.

Brendan Hansen avatar
Brendan Hansen
Brendan Hansen

Guest Author

Read more

waiEngineeringregistry

Testing - WAI

RudraMarch 14, 2023

wapmwebassemblyregistry

WebAssembly as a Universal Binary Format (Part II: WAPM)

Syrus AkbaryAugust 19, 2022

wapmwasmer registryengineeringregistry

WAPM: A Newly Renovated Home For WebAssembly

Syrus AkbaryMarch 2, 2022

wasmerwasmer edgerustprojectsedgeweb scraper

Build a Web Scraper in Rust and Deploy to Wasmer Edge

RudraAugust 14, 2023