The following document is a non-official (i.e. not governed by the Onyx Software Foundation) Onyx programming language reference (the Reference).

The Reference is meant for the Onyx programming language end-users, i.e. those willing to commit themselves into development of programms written in Onyx. The Reference is technical, but it may use informal language and also contain explainations, rationale and repetitions where needed.

The Reference is maintained by Fancy Software, the company behind the Fancy Onyx compiler. See the reference source code at GitHub. You can also reach @fancysoft and @vladfaust on Twitter for feedback.

The Onyx programming language

Onyx is a general purpose computer programming language inspired by C++ and Crystal.

Programs written in Onyx are designed to be executed on modern hardware. At the moment of writing, these are classic-processor computers running mainstream operating systems, such as Windows, OSX, Linux, iOS and Android. Onyx assumes presence of vital modern components such as MCU and FPU on a target machine. Once technology evolves in order to that another component is considered vital (e.g. NPU, QPU), the language shall be updated to reflect the evolution.

Still, Onyx is fit in more exotic environments. The language features low-level functionality such as pointer arithmetic and easy C ABI interaction; these are often marked unsafe and require explicit transferring of responsibility from a compiler to the programmer themselves. Wrapping the low-level code into safer APIs allows to perform efficiently while staying on the higher level of mindset.

Design principles

Onyx defines the following design priniciples (essentially one).

Reuse as much as possible

Reusing concepts dramatically flattens the learning curve. The principle is applicable to all aspects of the language. For example, a runtime variable, a function argument, a class field and even a template argument all share the same declaration syntax. Interoperability is almost transparent; C types, C literals and even C macro definitions may be used as-is. However, sometimes evolution requires greater changes.

Be honestly friendly

A good friend wishes you the best and he is being honest about that. Sometimes, a good design requires shift in mind; sometime, it’s rough. Sometimes, explicitness is nessecary. Otherwise implicit, inferrable details may be omitted to improve the interface. That’s the way technology evolves.

Embrace the ever-growing technology

Some think that Int32 is more ambigous than stricter SInt32. Others prefer the punk-ish C approach and name the things without thinking about the eternity we all are having joy for here. 64 bits isn’t nearly enough to work with qubits comfortably, also Y2038. What seemed unreachable before now is true. One moment in the future, there will be some Int\$2^32\$ notation to denote a binary integer with infinite-scalable radix. Until then, stay fast and practical.

Reference conventions


The Standard doesn’t mandate whether Onyx is a statically- or dynamically-compiled language. It is designed to fit both scenarios. An application able to run programs written in Onyx is referenced to as a compiler for brewity. In shell code examples, fnx implies the Fancy Onyx compiler.

Compile-time output (e.g. panic or macro output) is denoted with =>, e.g. # => Panic! Undeclared variable. Runtime output is denoted with ->, e.g. # -> Hello, world!.

🚧 Current state

The Reference is WIP, its development is aligned to the Fancy Onyx compiler. Most concepts are stabilized. Builtin and standard libraries aren’t complete yet, though, their usage in examples is mostly for demonstration purposes.

Currently, the Reference may be too dry and technical, i.e. more of a specification rather than a user-friendly reference. You can improve that by creating a PR in

1. Compilation

The whole Onyx program is compiled at once without separating header and source files. Source files are cross-referenced by the rules defined in Section 1.1, “Onyx source files” and Section 1.2, “C source files”.

A compiler panics, i.e. halts compilation, if the program is ill-formed. The Standard defines panic unique identifiers to aid debugging, e.g. P001: Undeclared function reference.

Meta path

A meta path is a file path beginning with a seemingly-relative file or directory name, e.g. "foo.nx" or "foo/bar.nx", but neither "./foo.nx" nor "/foo/bar.nx" nor "C://baz.nx".

The way a meta path is resolved is implementation-defined. Usually it is resolved by looking up in locations defined by some flags passed to the compiler binary.

If Onyx packages are stored in some ./onyx_modules directory, and there is a source file at ./onyx_modules/my_module/main.nx, the Fancy Onyx compiler would expect an -i./onyx_modules flag to make an import "my_module/main.nx" directive work.

1.1. Onyx source files

Onyx uses modules mechanism similar to ES6 modules. Program entities may be exported using an \$tt "export"\$ directive, and imported using an \$tt "import"\$ directive.

By convention, an Onyx source file has .nx extension. No extension is suffixed implicitly upon file lookup.

The Standard defines "std" and "std/*" extension-less meta path family, which are reserved for cross-platform and OS-specific API standard entity implementations, accordingly. See [_std_api].

Example 1. Modules
export default def sum(a, b : Int32) {
  return a + b
import { out } from "std/term"
import sum from "./sum.nx"
out << sum(1, 2)
$ fnx ./main.nx

1.1.1. Circular imports

Circular imports are allowed in Onyx. A compiler jumps back to the file upon detecting an undeclared-yet entity unless dead-loop is detected.

Example 2. Circular imports
import Post from "./post.nx"

export default class User {
  final posts = List<Post>.new()
  #                  ^ Jump point

  let name : String

  def top_rated_post() -> Post {
    #                     ^ Jump point

    let top_rating = 0u
    let top_index = 0z

    this.posts.each#indexed((p, i) => {
      if (p.rating > top_rating) {
        #   ^ Jump point

        top_rating = p.rating
        top_index = i

    return this.posts[top_index]
import User from "./user.nx"

export default class Post {
  final author : User
  #              ^ Jump point

  let rating : UInt32

  def author_name() -> {
    # ^ Jump point

1.2. C source files

A file referenced by a C include directive within an Onyx \$tt "extern"\$ directive, as well as a file included by a C file, is looked up in a similar fashion as it would be if compiled by a C compiler. That said, meta paths are applicable to C files lookup as well.

The Fancy Onyx compiler makes use of the conventional -I flag, e.g. -I/usr/include to resolve C paths.
The Standard states that using an Onyx compiler binary would neither require to link standard C libraries explicitly nor to provide the system-dependent standard C include paths, unless cross-compiled. Therefore, the -I/usr/include flag would be implied on a Linux host when using the Fancy Onyx compiler.


A comment begins with # and spans until the end of the line.

# This is a comment.
some_call() # This is also a comment

A # immediately following an identifier is considered a function tag; an explicit spacing would be required.

list.each#indexed() # A tagged function call, `#` denotes a function tag
list.each #indexed() `#` begins the comment

2.1. Documentation

Multiple comments without empty lines adjacent to a statement are called documentation. An annotation application between documentation and statement doesn’t break the documentation.

Documentation blocks may be parsed by a documentation generator to generate a user-friendly API documentation.

# This is a freestanding comment.
# It wouldn't be seen in the API docs.

some_call() # This is also a comment which wouldn't be present in the API docs

# This is a documentation block.
# It would be reflected in API docs.
class MyClass {
  # This is a documentation for the variable,
  # also seen in the API docs.
  let foo : String

2.2. Comment intrinsics

TODO: :ditto:.

2.3. Comment styling

Enforcing comment styling increases the quality of Onyx libraries.

A comment should be written in British (en_gb) language, but may contain any other Unicode chars for emojis, names, charts etc.

A freestanding comment should always have one empty comment line appended. A documentation may or may not have a empty comment line appended.

A non-inline comment should be hard-terminated with an optional sequence of emojis following the terminator. An inline comment should not be hard-terminated.

Inline comments in adjacent lines should be aligned to the fartest one. If an inline comment hits some pre-defined length limit, it disables the alignment for the whole block of adjacent lines.

Comments support vanilla Markdown styling. By default, a code fence block language is Onyx.

Example 3. Comment styling
threadsafe! {
  # This context is threadsafe.

  x.value += 1          # Implicitly wrapped in class mutex thus slow
  fragile! x.value += 1 # Fast but not threadsafe

  Atomic.incr(&x) # Increment atomically with sequentially consistent ordering
  fragile! Atomic.incr<:release>(&x) # Increase atomically with "release" ordering

But this is also valid styling:

threadsafe! {
  # This context is threadsafe. 🤗
  x.value += 1 # Implicitly wrapped in class mutex thus slow
  fragile! x.value += 1 # Fast but not threadsafe
  Atomic.incr(&x) # Increment atomically with sequentially consistent ordering
  fragile! Atomic.incr<:release>(&x) # Increase atomically with "release" ordering

3. Scope

A scope is comprised of scope members.

Only an imported or declared in the same file, or a builtin entity identifier may be looked up from within a scope. If an identifier is failed to be located in current scope, then the parent scope may be looked up recursively in accordance with rules specific for the scope; for example, an object method can not lookup the object’s fields directly. Variable shadowing is always prohibited. Closures are explicit in Onyx, see Section 5.7, “Lambda”.

Static scope of a type specialization is queried via the static lookup operator x::y. An type specialization instance (i.e. an object) can be queried for a method via the prototype lookup operator x:y; it would require \$tt "this"\$ to be explicitly passed upon a call (see UFCS). Alternatively, an object may be queried for its members via the object lookup operator x.y.

struct Foo {
  static def bar() -> 42 # Explicitly static scope of this specialization
  def baz() -> self::bar # Implicitly instance scope of this specialization

let foo = Foo()

  Foo::bar() ==    # Static lookup
  Foo:baz(foo) |== # Prototype lookup
  foo.baz()        # Object lookup

An static type specialization member declaration should be accessed using the \$tt "self"\$ keyword, which evaluates to the containing type.

let x = "hello"

struct Foo {
  static let bar = 42

  def baz() {
    x               # OK, declared in the file
    # let x = "bye" # => Panic! Would shadow `x`

    # bar        # => Panic! Undeclared `bar`. Did you mean `self::bar`?
    let bar = 69 # OK, a function-local variable doesn't shadow

    return self::bar |== Foo::bar # OK, return the static variable

extend Foo {
  # static let qux = bar # Panic! Undeclared variable `bar`. Did you mean `self::bar`?
  static let qux = (Foo::bar |== self::bar) # OK

  static def quux() {
    # baz() # Panic! Undeclared function `baz`. Did you mean `self::baz`?
    return Foo::baz() |== self::baz() # OK

\$tt "this"\$ keyword is used within a type method, which evaluates to the caller instance (or an immutable copy of the caller if it is a struct). An instance field can never be directly accessed from a method, only via querying the instance.

struct Foo {
  let bar = 42
  let baz = bar + 27      # OK, because in the same scope
  let baz = + 27 # Also OK

  def qux() {
    # bar          # => Panic! Undeclared variable `bar`, did you mean ``?       # OK
    qux()          # OK, defined in this scope
    this.qux()     # Also OK
    Foo:qux(this)  # OK (UFCS)
    self:qux(this) # Ditto

To make a struct function callable on a struct pointer, you should explicitly define a static function accepting a pointer argument. Thanks to UFCS, when called on a pointer, the caller would be passed implicitly. A getter and setter accepting a struct pointer are implicitly defined for a struct field. See Section 7.3, “Field” and Section 5.2, “Pointer”.

struct Point {
  let x = 1f

  static double(ptr : Point*lw) -> {
    *ptr.x *= 2 # I.e. `Point::x*=(ptr, 2)`, which is implicitly defined

let point = Point()
let pointer = &point : Point*lw

pointer->double() # `Point::double(pointer)`
assert(point.x == 2)

3.1. Safety

It is illegal to call a lower-safety code from within a higher-safety context, unless wrapped in an explicit safety statement.

# An explicitly fragile block to emphasize (top level is fragile by default).
fragile! {
  # $call() # => Panic! Calling C function is unsafe

  unsafe! $call()     # OK, a single C call is explicitly unsafe
  unsafe! { $call() } # OK, the whole block is explicitly unsafe

3.1.1. Unsafe

Unsafe is the lowest possible safety level. An unsafe scope is similar to a fragile scope in regards to multi-threading, i.e. there are no any ordering guarantees. The list of unsafe operations follows.

3.1.2. Fragile

A fragile scope has causal invariance, thus undefined memory access ordering. Therefore, a fragile code is safe to execute in a single thread, but multi-threaded execution may lead to race condition. See Atomic for explicit ordering control instruments.

The top level scope is guaranteed to run sequentially thus fragile. Any Onyx function definition has fragile safety by default. It is legal to overload a function by its safety; many builtin types have multiple safety overloads.

List of fragile operations:

  • Accessing a non-final static variable or struct field;

  • Dereferencing a static writeable pointer;

  • Accessing a class field.

3.1.3. Threadsafe

A threadsafe context implies that the code may be simultaneously executed from multiple threads. Consequentially, in a threadsafe context code implicitly has the strongest ordering guarantees.

List of threadsafe operations:

  • Accessing a local variable or local struct field;

  • Dereferencing a static readonly pointer;

  • Dereferencing a pointer with local storage.

3.2. Visibility

Visibility may be either public or private.

By default a variable or function is public. It may be defined explicitly private using the \$tt "private"\$ modifier. It is not possible to access a private member of a scope other than by wrapping the access in some sort of public getter. A visibility modifier can not be overloaded, only overriden; with an exception for a class constructor.

struct Foo {
  def bar();            # Implicitly public by default
  private reimpl bar(); # Now it's private

# Foo().bar() # => Panic! Can not access private field ``

3.3. C scope

C entities (functions, types and even literals) are referenced from Onyx code by prepending $. Multi-word entities are wrapped in backticks, e.g. $`unsigned int **`.

Builtin C declarations (i.e. those not requiring any includes) are accessible anywhere. For example, $int, $char* etc.

A non-builtin C declaration requires current Onyx source file to either include the header, or to declare the entity explicitly. That means, for example, that you have to extern #include "stdio.h" or extern void puts(char*); in each Onyx source file which calls $puts.

4. Type system

In computer science and computer programming, a data type or simply type is an attribute of data which tells the compiler or interpreter how the programmer intends to use the data. […] A data type constrains the values that an expression, such as a variable or a function, might take. This data type defines the operations that can be done on the data, the meaning of the data, and the way values of that type can be stored.

— Wikipedia conributors

A data type in Onyx is complex: it is comprised of real and virtual parts. The real type defines memory layout of the data, while virtual type defines the way the data behaves. A virtual type is always the same or narrower-than the real type.

Given that there is a variable x of type Float64~Real, the memory layout of x would be defined by the built-in Float64 type, that is, 64 bits. But it would behave in accordance to the virtual Real trait restriction, which is the common trait of all real numbers (\$RR\$). Effectively, this would, for example, disallow calling the floating-point-specific .round() method on x.

In contrary, if y was simply of type Float64 or Float64~Float or even Float64~Float64, the .round() method would remain accessible. A \$tau\$~\$tau\$ type can be written as simply \$tau\$.

let x : Float64~Real = 42.0
# x.round() # Panic! Undeclared method `Float64~Real.round()`

let y : Float64 = 42.0
y.round() # OK
The \$tau\$ : \$upsilon\$ notation is taken directly from type theory. The spacing is required because the notation also acts as an actionable restriction operator. A colon adjacent to an identifier is a distinct entity called label, which is used for argument aliasing.

4.1. Virtualization

An application of an virtual type restriction for the purpose of narrowing down a runtime data behaviour is called virtualization. Virtualization reduces ambiguity, improves code readability and ensures long-term project maintainability.

Example 4, “Simple virtualization” introduces the reader to the concept of virtualization.

Example 4. Simple virtualization
let x : Float64 = 42.0

# `Float64.round` is declared.
x.round() # OK

# Temporarily virtualizing `x` to `~Real` removes
# the ability to call `Float64`'s methods.
# x~Real.round() # => Panic! Undeclared `Float64~Real.round`

# Storing a virtualized type permanently.
# `y` type is inferred to be `: Float64~Real`.
let y = x~Real
# y.round() # => Panic! Undeclared `Float64~Real.round`

Virtualization can be used to solve ambiguity. For example, when a type implements multiple traits with the same method name, virtualizing an instance to a trait type would help selecting the desired method.

Also, a virtual restriction is used in template types, more on that later.

When a switch case is a virtual type, the switcher is virtualized to that type within the case body. Alternatively, a type may be checked using the virtual type comparison operator ~?, which evaluates to \$tt "true"\$ if the type matches the virtual restriction, \$tt "false"\$ otherwise. In other words, if a virtual type comparison is contained within a condition, and there are guarantees that the data won’t be altered, then the virtualization applies within the branch. It is illegal to directly apply a non-narrower virtualization, it must be queired with ~? or narrowed in some other way beforeahead (see the example below).

A single type is a degratory case of a type expression following the boolean logic rules. \$and\$ (&&), \$or\$ (||) and \$neg\$ (!) operations are implemented, which may be grouped in parentheses. Complex expressions are used to narrow the type even more.

Example 5, “Advanced virtualization” is more complicated, but it demonstrates how virtualization ensures long-term maintainability of programs written in Onyx. The code below would continue working even if added another Drawable4D trait with the same draw() method.

Example 5. Advanced virtualization
# Both traits have the same-named method declaration.
trait Drawable2D { decl draw() }
trait Drawable3D { decl draw() }

struct Figure { }

struct Square : Figure, Drawable2D {
  impl draw() { } # Implements the `Drawable2D`'s method

struct Cube : Figure, Drawable2D {
  impl draw() { } # Also implements the `Drawable2D`'s method

# ...

# Later on we decide to also make a cube drawable in 3D.
# It would break existing `cube.draw()` calls.
extend Cube : Drawable3D {
  # impl draw() { }                   # => Panic! Ambiguous
  impl (self ~ Drawable3D).draw() { } # OK, virtualized to select the one we need
  # impl self~Drawable3D.draw() { }   # A legal shortcut
  # impl ~Drawable3D.draw() { }       # Or even shorter

# Now, let's run some code.

let square = Square()
square.draw()            # OK, call the 2D implementation
square~Drawable2D.draw() # Ditto

let cube = Cube()
# cube.draw()          # => Panic! Ambigous (that's the effect of deriving `Drawable3D`)
cube~Drawable2D.draw() # Call the 2D implementation explicitly
cube~Drawable3D.draw() # Call the 3D implementation explicitly

# def draw<T ~ Figure>(figure : T) # Can be shortcut:
def draw(figure ~ Figure) {
  if (figure ~? Drawable2D) {
    @debug<@typeof(figure)>()  # => T~(Figure && Drawable2D) (note the expression)
    figure.draw()              # Always call the 2D implementation
    figure~Drawable2D.draw()   # Ditto
    # figure~Drawable3D.draw() # => Panic! Can not virtualize to `Drawable3D`

    switch (figure) {
      case ~Drawble3D {
        @debug<@typeof(figure)>() # => T~(Figure && Drawable2D && Drawable3D)
        # figure.draw()           # Panic! Ambigous call
        figure~Drawable2D.draw()  # Have to choose now
        figure~Drawable3D.draw()  # OK
      } else {
        @debug<@typeof(figure)>()  # => T~(Figure && Drawable2D && !Drawable3D)
        figure.draw()              # Always call the 2D implementation
        figure~Drawable2D.draw()   # Ditto
        # figure~Drawable3D.draw() # => Panic! Can not virtualize to `Drawable3D`
  } else {
    @debug<@typeof(figure)>()  # => T~(Figure && !Drawable2D)
    # figure.draw()            # => Panic! Undefined `T~Figure.draw`
    # figure~Drawable2D.draw() # => Panic! Can not virtualize to `Drawable2D`
    # figure~Drawable3D.draw() # => Panic! Can not virtualize to `Drawable3D`

  @debug<@typeof(figure)>()  # => T~Figure
  # figure.draw()            # => Panic! Undefined `T~Figure.draw`
  # figure~Drawable2D.draw() # => Panic! Can not virtualize to `Drawable2D`
  # figure~Drawable3D.draw() # => Panic! Can not virtualize to `Drawable3D`

It may be seen that it’s up to a programmer to decide on the virtualization level in their projects based on to what extent the program is expected to grow in the future.

4.2. Type restriction

The instance type restriction operator : is used to:

  • Declare a variable type, e.g. let x : \$tau\$;

  • Declare a function runtime argument type, e.g. def foo(arg : \$tau\$);

  • Declare a function return type, e.g. def foo() : \$tau\$;

  • Ensure a data type within an expression, e.g. let x = 42 : Int32, which also makes the code self-documenting;

  • Narrow down a return-overloaded function, e.g. : String;

  • Declare the list of a type ancestors, e.g. class Cow : [Animal, Cattle] (see [_inheritance]).

A runtime data type must always be concerete, i.e. have a real type defined. In contrary, a template argument may only be virtually restricted.

For convenience, a function argument is allowed to have an imaginary type restriction. An anonymous template argument is implied in this case, i.e. def foo(arg ~ \$upsilon\$) \$-=\$ def foo<T ~ \$upsilon\$>(arg : T).

In addition to ~?, a real data type may be compared using the real comparison operator :?. In fact, it accepts a complex type, e.g. x :? \$tau\$~\$upsilon\$, but the virtual part may be omitted. The operation evaluates to a boolean literal. A compiler also takes the comparison in consideration, narrowing the type if it is a case branch comparison, for example.

def foo(arg ~ Real) {
  if (arg :? Int32) {
    print("Is Int32")
    arg : Int32~Int32 # OK, narrowed down


foo(69)   # -> Is Int64
foo(42.0) # => Float64~Real

4.3. Record

A record is a non-empty stack-allocated collection of named heterogeneous elements, accessible as fields. A record type \$R\$ defines a public constructor which accepts an \$R\$ instance with fields passed by their names. Also, Record<\$R\$> : \$R\$.

A record field definition is implicitly \$tt "let"\$ by default. A \$tt "final"\$ field can not be modified after record instantiation. See Section 7.3, “Field”.

let rekord = { x: 1, final y: 2 } :
  Record<{ let x : Int32, final y : Int32 }> :
  { x, final y : Int32 }

assert(rekord.x == 1)
rekord.x = 3
assert(rekord.x == 3)

assert(rekord.y == 2)
# rekord.y = 4 # => Panic! Can not modify final field `rekord.y`

If a record field \$x\$ has a default value, then it may be omitted in the constructor record. A record type which has default values set for all of its fields also defines a zero arity constructor. A default value is not a part of the record prototype, i.e. { \$x\$ : \$tau\$ = \$a\$ } \$-=\$ { \$x\$ : \$tau\$ = \$b\$ } \$-=\$ { \$x\$ : \$tau\$ }. The nearest default value is applied upon construction.

A record type may be aliased. Recursive aliasing is not allowed unless it’s a recursive pointer, e.g. alias Node => { parent : Node* }.

A record defines the equality operator === which returns true if all of its fields are equal to the operand’s. !== returns true if any of the fields aren’t equal. |=== is similar to ===, but returns a copy of self if equal, useful in long assertions.

alias Point => { x, y : Int32 = 0 }

  Point({ x: 0, y: 0}) ===
  Point({ x: 0 }) |===
  Point({ }) |===

assert(Point() !== Point({ x: 1 })) # `x` aren't equal

To avoid repetition, a freestanding identifier \$i\$ expands to \$i\$: \$i\$ in a record.

let x = 17
final point = Point({ x }) # Shortcut to `Point({ x: x })`
assert(point.x == 17)

A record type can also be aliased disinctively, which allows its extension and thus inheritance. A record fields have undefined order, but an ancestor’s field are guaranteed to come earlier. This effectively allows to safely upcast a record instance.

To ensure memory ordering for interoperability, use C structs instead.

For a record type \$R\$ a static function \$R\$::\$R\$ may be oveloaded to implement a custom constructor. Inherited fields are available in a constructor record.

A non-identifier \$x\$ (e.g. a call) of type \$tau\$ may be passed to a record constructor anonymously iff the record has \$tau\$ in the list of its ancestors; the ancestor would be initialized with \$x\$ then and merged with other fields in order of appearance in the constructor record (former values are overwritten).

enum Color {

struct Drawable {
  let color : Color

distinct alias Point => { x, y = 0f } : Drawable {
  # A custom constructor.
  static self(x, y, color) -> self({ Drawable({ color }), x, y })

  # A custom method available exclusively for a `Point` instance.
  def length() -> (this.x ** 2 + this.y ** 2).sqrt()

# let point = Point({ Drawable({ color: :red }), x: 3, y: 4}) # Too long 👎
let point = Point(3, 4, :red) # Calling the custom constructor 👍

assert(point.color == :red)
assert(point.length() == 5)

final drawable = point as Drawable # OK, safe
# point = drawable as Point        # => Panic! Can not safely downcast

A more ergonomic alternative to distinctly aliasing a record type would be defining a freestanding struct instead.

You can also declare method on a simply-aliased record type using an out-of-scope declaration. However, it would declare the method for all instances of the record, which is rarely desirable.

alias Point => { x, y : Float64 }
def Point.length() -> (this.x ** 2 + this.y ** 2).sqrt()

# Do you really want this method for all of the similar records?
assert({ x: 3 : Float64, y: 4 : Float64 }.length() == 5)

4.4. Type declaration

A type can be declared using a \$tt "decl"\$ statement. A type declaration requires its category to be declared, one of \$tt "struct"\$, \$tt "class"\$, \$tt "enum"\$, \$tt "flag"\$, \$tt "trait"\$, \$tt "unit"\$ or \$tt "annotation"\$.

A type identifier rules are similar variables, i.e. /[a-zA-Z_](a-zA-Z0-9_)*/. A type identifier also may be wrapped in backticks to allow Unicode characters in it. By convention a type name begins with an uppercase letter. The only exceptions are keywords\$tt "nil"\$, \$tt "void"\$ and \$tt "discard"\$.

A type declaration does not contain body. A declared type doesn’t have size hence incomplete. Still, it may act as a virtual type restriction, or a template argument which doesn’t need to be complete (e.g. a pointer type).

decl struct Point
# Point()            # => Panic! Can not construct an incomplete type instance
let pointer : Point* # OK, a pointer to incomplete type is legal

To be able to make use of a declared type, it shall be implemented. An attempt to implement an undeclared yet type panics. An implemented type becomes complete hence usable in runtime.

impl Point {
  let x, y : Float64
  def length() -> (this.x ** 2 + this.y ** 2).sqrt()

let point = Point({ x: 3, y: 4 })
assert(point.length() == 5)

Once a type is implemented, it may not be implemented again. Multiple non-intersecting partial implementations of the same type are allowed, which is only applicable to a template type.

To extend an existing implementation, an \$tt "extend"\$ statement shall be used. Attempt to extend an unimplemented yet type panics. An annotation type can be neither implemented nor extended.

decl struct Foo<T>

forall [T ~ Real] impl Foo<T> {
  let real : T = 0

# Already implemeneted for all `~ Real`, `Int32` is `~ Real`.
# impl Foo<Int32> { } # => Panic! Already implemented

# Should extend instead.
extend Foo<Int32> {
  let int_specific : Int32 = 0 # Define a field exclusively for this specialization

# Foo<Float64>().int_specific # => Panic! Undefined `Foo<Float64>.int_specific`
Foo<Int32>().int_specific     # OK

It is tempting to allow an explicit \$tt "reimpl"\$ statement for an already implemented type, which would obliterate all previous member definitions. However, this would reduce the program maintainability in a longer run. Therefore, reimplementing a type in Onyx is illegal. It is still legal to reimplement a function, though.

4.5. Template type

A type may be declared with template arguments, which makes it a template type.

A template argument declaration \$tau\$ is accessible within the same scope, and also directly under the type identifier, e.g. \$tt "self"\$::\$tau\$. A template argument may have an alias label, which is referenced from outside.

A data type itself (not an instance) may be compared using static type comparison operators \:? and \~? which are similar their instance counterparts :? and ~?.

Once declared, a template type should be implemented for a subset of unique combinations of its template arguments.

# A template type `T` with `Type` alias.
decl struct Foo<Type: T>

# Implement for `Int32`.
impl Foo<Int32> {
  let x : Int32

# Publicly accessible by the alias.
assert(Foo<Int32>::Type \:? Int32)

# Implement for `String`, reference by the alias.
impl Foo<Type: String> {
  let x : String

assert(Foo<String>::Type \:? String)

A subset of template argument combinations may be implemented using a \$tt "forall"\$ modifier. The modifier declares a new list of template arguments which are passed to the template type, optionally wrapped in square brackets.

decl struct Foo<T>

forall T impl Foo<T> {
  let x :  T

Declaration and implementation may be done simultaneously using a \$tt "def"\$ statement, which is often omitted if type category is present. When defining a type, the implementation is implicitly for all possible template arguments.

struct Foo<T> {
  let x : T

A template argument may have a restriction, either real or virtual. Within a specialization, the argument type would have the restriction applied. A compiler panics on a specialization lookup query failure.

decl struct Foo<T ~ Real> # Declare for all types matching `~ Real`
# impl Foo<String> { }    # => Panic! `String` doesn't match `~ Real`
impl Foo<Int32> { }       # OK

def struct Bar<T ~ Real> { } # Define for all types matching `~ Real`
Bar<Int32>(42)               # OK

A template argument \$tau\$ declared with a \$tt "forall"\$ \$tau\$ modifier wouldn’t be accessible from outside of current implementation.

forall T decl Foo<Type ~ List<T>>

impl Foo<List<Int32>> {
  # T                       # => Panic! Undeclared `T`
  self::Type \: List<Int32> # OK

forall T struct Bar<Type ~ List<T>> {
  T \: self::Type \: Type \: List<T> # OK

A type is specialized once it’s referenced with all its template arguments (if any) specified or inferred. Each unique combination of template arguments leads to a distinct type specialization. During specialization, delayed macros are evaluated within the matching type implementation and its extensions, in order of appearance in source code.

struct Foo<T> {
  \{% puts "Specialized!" %}

Foo<Int32>()  # => Specialized!
Foo<Int32>()  # (Already specialized)
Foo<String>() # => Specialized!

Intersecting type implementations are illegal. Intersecting extensions are legal, though, and would layer up.

decl Foo<T>
forall [T ~ Real] impl Foo<T>;
# forall [T ~ Int] impl Foo<T>; # Panic! Already implemented for `T ~ Real`
forall [T ~ Int] extend Foo<T>; # OK

\$tt "self"\$ keyword query without template args implies the caller site-defined template args, i.e. "this specialization". \$tt "self"\$<> rewrites the template arguments, i.e. "that specialization".

def struct Int<Bitsize: Z> {
  # Implies `def add(another : self<Z>) : self<Z>`.
  def add(another : self) : self

4.6. Type inheritance

A type \$tau\$ may inherit another type \$alpha\$ upon declaration, implementation or extension using the \$tau\$ : \$alpha\$ notation. Inherited types are comma-separated and may optionally wrapped in square brackets.

A member with an explicit \$tt "static"\$ modifier is not inherited.

trait Enumerable<T> {
  decl each(block : T => discard) : void

# Inherits `Enumerable` and proxies the template argument to it.
class List<U> : Enumerable<U> {
  # Implicitly `each(block : U => discard)`.
  impl each(block) {
    let i = 0z
    while (i < this.size)

If an inherited type contains a field, the field then can be defined again with exactly the same type in the inheriting type. Fields have undefined order, but an ancestor’s field are guaranteed to come earlier. A field default value may be overriden or even removed in an inheriting type.

See Section 4.8, “Struct” and Section 4.9, “Class” for constructor documentation.
struct Foo {
  let foo : Int32 = 0

struct Bar : Foo {
  let foo : Int32 = 1 # Override the default value

struct Baz : Foo {
  let foo : Int32 # Undefine the default value completely

assert(Foo().foo == 0)
assert(Bar().foo == 1)

# Baz()                          # => Panic! Undeclared `Bar::Baz()`
assert(Baz({ foo: 0 }).foo == 0) # OK

Referencing an ambigous method declared in an inherited type would require virtualization.

trait Foo {
  decl foo()

trait NotFoo {
  decl foo() # Note the same name
  decl bar()

struct Bar : Foo, NotFoo {
  # impl foo() { }    # => Panic! Ambigous
  impl ~Foo:foo() { } # OK, being specific
  impl bar() { }      # OK, no ambiguity

# Bar().foo()   # => Panic! Ambigous
Bar() # OK
Bar().bar()     # OK

See type categories documentation for category-specific inheritance rules.

4.7. Trait

A trait type defines behaviour of a deriving type, i.e. its methods; it doesn’t define fields.

A trait member storage is defined by a deriving type. However, it can be declared with an explicit \$tt "static"\$ modifier, which would make the member un-deriveable; only a \$tt "static"\$ variable may be defined in a trait, a field definition is prohibited.

A struct, class, unit or trait type can derive a trait. A trait type by itself is incomplete hence can not be used in runtime on its own. \$tt "this"\$ within a trait’s method evaluates in the context of the deriving type. Ditto for \$tt "self"\$.

trait Foo {
  decl foo()

  def get_foo() -> {

A trait type may inherit another trait.

A unit type may inherit a trait. The trait’s methods become "instance" methods of the unit type. Explicitly \$tt "static"\$ trait members aren’t derived.

trait Foo {
  decl foo()
  def bar() ->
  static def baz() -> "qux"

unit Unit : Foo {
  impl foo() -> 42

assert( == 42) # OK
# Unit::baz()            # Panic! Undeclared `Unit::baz()` (static members aren't derived)

4.8. Struct

A struct is an ergonomic alternative to a to distinct record alias. Akin to record, a struct is passed by value.

A default public struct constructor accepts a record containing the struct’s fields, or zero args if the record is empty.

struct Point {
  let x, y = 0f
  static zero() -> self()

  Point({ x: 0, y: 0 }) ===
  Point({ }) |==
  Point() |==

A struct type may inherit another struct or trait.

To make a struct function callable on a struct pointer, you should explicitly define a static function accepting a pointer argument. Thanks to UFCS, when called on a pointer, the caller would be passed implicitly. A getter and setter accepting a struct pointer are implicitly defined for a struct field. See Section 7.3, “Field” and Section 5.2, “Pointer”.

struct Point {
  let x = 1f

  static double(ptr : Point*lw) -> {
    *ptr.x *= 2 # I.e. `Point::x*=(ptr, 2)`, which is implicitly defined

let point = Point()
let pointer = &point : Point*lw

pointer->double() # `Point::double(pointer)`
assert(point.x == 2)

4.9. Class

A class instance is a record residing in a non-stack memory with automatic garbage collection.

Unlike struct or record, a \$tt "final"\$ class instance still allows its fields to be modified.

A class doesn’t have a public constructor implicitly defined, only a private one. A class constructor is the only case when visibility overloading is legal. If a class doesn’t contain any fields, a zero-arity public constructor is defined.

class Dog {
  let name : String
  static self(name) -> self({ name }) # Shall define a public constructor explicitly

final spot = Dog("Spott") # Oops = "Spot"        # OK. Note that `spot` is a `final` variable,
                          # but the non-final field can still be written

A class field may be of the same type as the class, i.e. a recursive class field is legal. Access to a class field or method is threadsafe unless the instance is passed outside of the stack.

Assigning a class instance is threadsafe and copies it by reference. An implicitly defined class equality operator === would return true if both operands point to the same memory region.

extend Dog {
  # Define a custom equavalence operator.
  def ==(another : Dog) -> ==

final spot_copy = spot
assert(spot === spot_copy) # Both equivalent...
assert(spot == spot_copy)  # and equal

final another_spot = Dog("Spot")
assert(spot !== another_spot) # Not equivalent, ...
assert(spot == another_spot)  # but equal

final mike = Dog("Mike")
assert(spot !== mike) # Neither equivalent...
assert(spot != mike)  # nor equal

A class instance is implicitly prefixed with an implementation-defined class header. Header size and layout are undefined, but shall remain the same for any class instance. Commonly a header contains RTTI and GC metadata.

A static class instance may be safely cast to a void pointer with static storage. A non-static instance pointer would have undefined storage instead. The pointer would point at the instance header followed by the record. The header offset value may be acquired via macro or \$tt "@classoffset"\$ intrinsic.

4.9.1. Class hierarchy

A class header contains runtime type information, (RTTI) which allows to have, for example, an array of class descendants without type erasure.

A class type may inherit another class, struct or trait. A class type declared with an \$tt "abstract"\$ modifier is incomplete, i.e. it can not be a variable real type. However, it still defines a private constructor to be used in a descendant.

A class instance may be threadsafely upcast to an ancestor class type using the \$tt "as"\$ operator. For runtime casting use the \$tt "@cast"\$ intrinsic and its nilable counterpart \$tt "@cast?"\$. Based on the class storage, an intrinsic invocation may be threadsafe (for an in-scope class instance) or fragile (all other cases).

Determining a local-only class instance actual type virtualizes it.

abstract class Animal {
  final intelligence : Float64
  decl sound() -> String

class Dog : Animal {
  static self() -> self({ intelligence: 0.3 })
  def bark() -> "Woof!"
  impl sound() -> this.bark()

class Cat : Animal {
  static self() -> self({ intelligence: 0.25 })
  def meow() -> "Meow!"
  impl sound() -> this.meow()

let animal : Animal = Dog()       # OK, can autocast
let animal = Dog() as Animal      # OK, static upcasting is always threadsafe
let animal = @cast<Animal>(Dog()) # OK, dynamic upcasting as an alternative

let dog = @cast<Dog>(animal)               # May throw if it's not a `Dog` instance, fragile
let maybe_dog = @cast?<Dog>(animal) : Dog? # Would return `nil` if failed to cast

final animals = List<Animal>()
animals.push(dog) # OK, can autocast

animals.each(a => {
  switch (a) {
    case Dog then @cast<Dog>(a).bark() # Dynamically downcast, fragile
    case Cat then @cast<Cat>(a).meow() # Ditto

  a.sound()             # OK, can call a declaration on an abstract ancestor type
  print(a.intelligence) # Also OK

4.9.2. GC

The Standard states that a class instance shall be garbage collected (GC), but it doesn’t define the precise mechanism. To aid the development of a GC implementation, Onyx defines private \$tt "finalize"\$ method and public \$tt "eachfield"\$ generator for a record, struct or a class type.

A private \$tt "finalize"\$ method (also known as destructor) is called implicitly once a class instance is deemed to be ready for garbage collection, i.e. finalized. A class type has \$tt "finalize"\$ method implemented by default; it invokes \$tt "finalize"\$ on each field yielded by an \$tt "eachfield"\$ generator call.

An \$tt "eachfield"\$ generator is implemented implicitly for most of the types. By default, it yields each field of the containing type. An \$tt "eachfield"\$ call is threadsafe on a local object, otherwise fragile.

An explicit \$tt "eachfield"\$ reimplementation may be required. For example, a dynamic array implementation would yield its pointee elements instead.

# A dynamic array.
class List<T> : Enumerable<T> {
  private pointer : T*
  getter capacity, size = 0u

  # Yield each element of the list.
  reimpl eachfield(yield) => this.each(e => yield(e))

  # Destroy the list, decreasing RC of its elements
  # (see ARC) and then freeing the pointer.
  reimpl unsafe finalize() -> {
    this.eachfield(e => @decrc(e))
    unsafe! $free(this.pointer as void*)

4.9.3. ARC

An Onyx compiler may rely on automated reference counting (ARC) to implement garbage collection.

In consideration to this, \$tt "@incrc"\$ and \$tt "@decrc"\$ intrinsics are defined in Onyx to increase or decrease strong reference counter explicitly and recursively, which is useful during interoperation.

A struct is allowed to have a class field, which implies the need to update the field’s reference counter; an intrinsic invocation on a struct would proxy the update to all its class fields, even nested. An intrinsic invocation on a struct without any class fields would therefore be noop.

An Onyx library author shall treat any compiler as implementing ARC. The RC intrinsics are always legal.

Example 6. ARC

In this example, we are passing some Onyx Database instance outside, to the C world. To keep the instance alive, we’re incrementing the RC upon passing it; and decrementing it when the C environment doesn’t need the instance anymore. See [_interoperation].

extern {
  // Initialize a *db* instance.
  void db_init(void* db);

  // Calling this function would destroy the *db* instance.
  void db_destroy(void* db) {
    # The Onyx context is implicitly unsafe here.
    $destroy_db(db); # This is an Onyx call

def init_db() {
  let db = Database()

  # NOTE: Taking pointer is safe, it's the C call which is not.
  unsafe! $db_init(db as void*)

  return db

unsafe destroy_db(ptr : void*) {
  @decrc(ptr as Database)

4.10. Enum

In computer programming, an enumerated type […] is a data type consisting of a set of […] enumerators of the type. The enumerator names are usually identifiers that behave as constants in the language.

In Onyx, a enum type is ~ Int, : SSize by default. Enumerator values begin with 0 and increment by 1 unless explicitly set. A enumerator constant can be safely interpreted as the underlying type, but not vice versa.

enum Foo {
  Bar,     # Implicitly `Bar = 0,`
  Baz = 2, # Set the value explicitly
  Qux      # Implicitly `3`

let foo : Foo = Foo::Bar # OK, can assign a enumerator directly
foo = Foo(Foo::Bar)      # OK, can construct with a enumerator (won't throw)
foo = Foo(0)             # OK, but may throw `EnumError`

assert(foo as SSize == 0) # Can safely upcast

# 0z as Foo       # => Panic! Can not safely cast `SSize` to `Foo`
unsafe! 0z as Foo # OK, but unsafe

A symbol expands to a enumerator constant unless ambigous.

enum Foo {

enum NotFoo {

def foo(arg : Foo) { }
foo(:bar) # OK, expands to `Foo::Bar` without ambiguity

def any(arg ~ Foo || NotFoo) { }
# any(:bar)   # => Panic! Would ambigously expand to either `Foo::Bar` or `NotFoo::Bar`
any(Foo::Bar) # OK
any(:qux)     # OK (`NotFoo::Qux`)

A enum may inherit another enum or a type matching ~ Int, but it doesn’t inherit any Int members, though.

Example 7. Enum inheritance
enum Foo : UInt8 {
  Bar, # `= 0`
  Baz  # `= 1`

# Foo::Bar + 1         # Panic! Undeclared method `Foo:+()`

enum FooPlus : Foo {
  Qux # ` = 2`

FooPlus::Baz # OK, inherited
FooPlus::Qux # OK, the new enumerator value

Rust-like enums can be achieved using the distinct alias feature. Also see Section 5.5, “Variant”.

Example 8. Rust-like enum
distinct alias RustEnum => Variant<String, Int32> {
  def foo() {
    switch (this) {
      case String then this~String
      case Int32  then this~Int32

let rust_enum = RustEnum("foo") # Is really `Variant<String, Int32>("foo")`

4.10.1. Flag

A enum doesn’t support bitwise operations by itself. However, a \$tt "flag"\$ may be declared instead, which would define bitwise methods and operators accepting another flag value. All flag enumerator values shall be a power of two; it may only inherit an ~ UInt type, USize by default.

Example 9. Flag
enum Enum {
  Foo, # `= 0`
  Bar, # `= 1`
  Baz  # `= 2`

# Enum::Foo | Enum::Bar # => Panic! Undeclared `Enum.|()`

flag Flag {
  Foo, # `= 1` (`2 ** 0`)
  Bar, # `= 2` (`2 ** 1`)
  Baz  # `= 4` (`2 ** 2`)

(Flag::Foo | Flag::Bar) as USize == 3 # OK
(Flag::Foo.and(:baz)) as USize == 5   # OK

4.10.2. Enum template argument

A enum or flag (with adjacency supported) literal may be used as a template argument. Note that a template argument must not have the same name as the already declared enum.

Example 10. Enum template argument
enum Color {

struct Foo<Color: C ~ \Color> {
  def foo() -> C

assert(Foo<Color: :red>.foo() == Color::Red)
assert(Foo<Color: Color::Blue>.foo() == :blue)

A flag template argument accepts a flag mask, i.e. multiple flag values.

Example 11. Flag template argument
flag Mode {

struct IO<Mode: M ~ \Mode> { }

# Implement for readable.
impl IO<:read> {
  decl read()

# Implement for writeable.
impl IO<:write> {
  decl write()

# Implement for both readable and writeable.
impl IO<:read | :write> {
  decl common()

IO<:read>().read()            # OK
# IO<:read>().write()         # => Panic! Undeclared `IO<Mode::Read>.write`
IO<:read | :write>().read()   # OK
IO<:read | :write>().common() # OK

4.11. Unit

A unit type is a singleton object type. It always has zero size, and its identifier is its only instance.

A unit member practically has static storage, but it shall be queried as if it was an "instance" member, i.e. via .. A unit field shall always have its default value set, as it’s practically a static variable. An "instance" unit member access is always fragile.

A unit member declared with an explicit \$tt "static"\$ modifier may only be statically-accessed, i.e. via ::.

A unit type defines equivalence operator ===, which returns \$tt "true"\$ iff the operand is the same unit.

unit Unit {
  let var : Int32 = 0

  def get_var() {
    return this.var

threadsafe! {
  let u = Unit
  assert(u === Unit)

  fragile! u.var += 1
  assert(fragile! u.get_var() == 1)

A unit type may inherit a trait. The trait’s methods become "instance" methods of the unit type. Explicitly \$tt "static"\$ trait members aren’t derived.

trait Foo {
  decl foo()
  def bar() ->
  static def baz() -> "qux"

unit Unit : Foo {
  impl foo() -> 42

assert( == 42) # OK
# Unit::baz()            # Panic! Undeclared `Unit::baz()` (static members aren't derived)

4.11.1. nil

\$tt "nil"\$ is the only builtin unit type.

It is one of the three falsey values in Onyx, along with \$tt "false"\$ and \$tt "void"\$. However, unlike \$tt "void"\$, \$tt "nil"\$ is a unit type, therefore it can be used in runtime, which makes is the God type to-go.

extend nil {
  static antipattern? = true


A type may be suffixed with ?, which turns it into a nilable variant.

assert(User? \:? Variant<User, nil>)

4.12. Annotation

An annotation type may only be declared, but not implemented. Annotations are used in macros.

annotation MyAnnotation<T ~ \String> # `decl` is implied

class User;

\{% nx["User"].annotations[0].type["T"].value == "foo" %}

Application of an annotation type which inherits another annotation applies the inherited annotation to the entity the original annotation is being applied to.

annotation Foo<T ~ \String>
annotation Bar<T ~ \String, U ~ \UInt> : Foo<T>

@[Bar<"baz", 42>] # Also applies `Foo<"baz">`
class User;

{% print nx["User"]"Foo") %} # => true

5. Builtin type

A builtin type doesn’t require explicit importing, i.e. is always globally accessible; it is also likely to have a literal.

Following the general design principles of Onyx, FPU and MCU presence is assumed for a target machine. Therefore, builtin types include those relying on floating point, vector and dynamic memory access operations.

This section gives an overview of the builtin types.

5.1. Utility types

5.1.1. Bool

The boolean type with two values: \$tt"true"\$ and \$tt"false"\$; these are also literals. \$tt"false"\$ is one of the three falsey values in Onyx, along with \$tt"nil"\$ and \$tt"void"\$.

The memory layout of Bool is undefined. There are built-in method to convert to integers, including to a single Bit.

5.1.2. void

\$tt"void"\$ is a special type implying absense of value. Instead of having a zero size, void doesn’t have size at all.

The semantics is equivalent to such in C. In fact, \$tt"void"\$ is interchangeable with $void.

It is not possible to assign a void. The only exception is assigning to a void-able variant; in such cases \$tt"void"\$ acts as a unit type. A void-able variant Variant<\$tau\$, \$tt"void"\$> can be shortcut with \$tau\$??.

# It can be either `nil`, `User` or void.
var : Variant<User?, void> : Variant<nil, User??> = void

Along with \$tt"false"\$ and \$tt"nil"\$, \$tt"void"\$ is a falsey value entity.

5.1.3. discard

When used as a return type restriction, \$tt"discard"\$ allows the callee to return literally anything, but caller would always treat it as \$tt"void"\$. In practice, this means "return anything, it’d be discarded anyway" without enforcing the void return type.

decl foo(block : T => void) -> void
decl bar(block : T => discard) -> void

# [1, 2].foo(e => e * 2) # => Panic! Must return `void`, returns `Int32`

# The returned value of the block is still `Int32`,
# but it's discarded and thus legal.
[1, 2].bar(e => e * 2) # OK

The \$tt"discard"\$ keyword can be shortcut with _.

def sum(a, b) -> _ { a + b }
# let result = sum(1, 2) # => Panic! Can not assign a discarded value

5.1.4. Numbers

Onyx follows Wikipedia in terms of numeric types hierarchy.

Real numbers

In mathematics, a real number is a value of a continuous quantity that can represent a distance along a line (or alternatively, a quantity that can be represented as an infinite decimal expansion).

— Wikipedia contributors

In Onyx, a real number type \$RR\$ simply derives the Real trait which in turn is a distinct alias to the zero-dimension Hypercomplex<0> specialization.

Bultin real number types are are Int, Float and Fixed.

  • Int* — a signed binary integer with variable bitsize;

  • UInt* — an unsigned binary integer with variable bitsize;

  • Bit

  • Byte — alias to UInt8;

  • Float* — an IEEE 754 binary floating point number with variable bitsize;


1q8.2 : Fixed8<2>, 1uq8.2 : UFixed8<2>, 0xabq8.2

Hyper complex numbers

In mathematics, hypercomplex number is a traditional term for an element of a finite-dimensional unital algebra over the field of real numbers.

  • Hypercomplex<T, D> — a hypercomplex number;

  • Complex<T> : Hypercomplex<T, 1> — a complex number, e.g. 2 + 1j;

  • Quaternion<T> : Hypercomplex<T, 4> — a quaternion;

5.1.5. Ratio

Ratio<T> — a number ratio, e.g. 1 / 2 : Ratio<Int32>;

5.1.6. Range

Range<T> — a range, e.g. 1..10 : Range<Int32>;

5.2. Pointer

In Onyx, a Pointer type declares template arguments defining the pointee type, the pointer storage and its writeability. Safe operations are available for a subset of the pointer type specializations based on latter two.

A Pointer<Type, Storage, Writeable> type may shortcut as Type*\$sw\$; where \$s\$ is one of l for local storage, s for static storage, or u or empty for undefined (default) storage; and \$w\$ is either w for writeable, or W or empty for non-writeable (default). For example, Int32*uW : Int32*, a non-writeable undefined-storage pointer to Int32.

Syntax of taking a pointer at a variable in Onyx is similar to such in C, the &var operator is used. Taking a pointer at a local or static variable, or a local or static record or struct variable field, returns a pointer with same storage and writeability as the variable. A class field pointer has undefined storage, because the class instance may die.

TODO: Casting to safer storage is unsafe.

A pointer may be dereferenced using the *ptr operator, also similar to C. Assigning a dereferenced pointer copies the pointee value. Assigning to a dereferenced writeable pointer rewrites the pointee.

TODO: Dereferencing has safety based on storage. Can not store a dereferenced pointee, only use immediately; can not have dereferenced type. Special assignment, *ptr.field = 42T:field=(ptr, 42); see "field".

let x = 42
final ptr = &x : Pointer<Int32, "local", true> : Int32*lw

*ptr = 43      # Rewrite the pointee data, threadsafe
final y = *ptr # Copy the pointee data, also threadsafe

assert(y == 43)

5.2.1. Pointer storage

A pointer storage may be one of "local", "static" or "undefined".

A local pointer points at the program stack. Dereferencing a local pointer is threadsafe, but you can not pass it outside of the stack, i.e. assign it to a class field or to a static variable.

A static pointer is located elsewhere, but is guaranteed to be accessible with at least fragile-level safety for the whole lifespan of the program. Dereferencing a static pointer is fragile, but it can be passed around safely.

Dereferencing a pointer with undefined storage is unsafe, because it gives zero guarantees about memory placement and availability. An undefined pointer can be passed around safely; all other storages can be safely cast to an undefined.

TODO: Add instance storage.

5.2.2. Pointer autocasting

When assigned, a pointer may be safely autocast in accordance with Table 1, “Pointer storage autocasting”. The writeability modifer can also be autocast to false. For example, a read-only static pointer can not be safely cast to a writeable local pointer.

TODO: Explicit casting vs. passing as an argument (e.g. can pass instance as local arg).

The "instance" storage is yet to be documented.
Table 1. Pointer storage autocasting
Storage To "local" To "instance" To "static" To "undefined" Outer scope? Dereferencing

















Threadsafe (1)












(1) Static storage contains instance storage.

A class instance can be safely cast to a read-only \$tt"void"\$ pointer with "undefined" storage, suitable for interoperability.

5.3. String

TODO: String is similar to std::string in C++. UTF-8-encoded and null-terminated.

TODO: Unicode is likely to be a part of stdlib, but Onyx string literal are all Unicode. \x{} for explicit hexadecimal values. Brackets are always mandatory.

TODO: Formatting. "Invalid index #{ index }" calls .to<String>() on interpolations.

let foo = "bar"            # Would be `String`
# let foo : $char* = "bar" # Panic!
let foo : $char* = $"bar"  # OK (C literal)

5.3.1. Char

A Char is a type with size enough to hold any single Unicode character.

5.4. Builtin containers

A builtin container has its size known in advance and is thus allocated on stack. A container literal commonly allows trailing commas.

5.4.1. Tuple

A tuple is a non-empty stack-allocated ordered sequence of heterogeneous elements. A tuple literal is a sequence of comma-separated elements wrapped in parentheses with trailing comma allowed, e.g. (a, b).

A single-element tuple is called a monuple. A monuple can have its parentheses omitted. Therefore, Tuple<T> \$-=\$ (\$tau\$) \$-=\$ (\$tau\$,) \$-=\$ \$tau\$ \$-=\$ Tuple<(\$tau\$)> \$-=\$ Tuple<Tuple<\$tau\$>> etc.

A tuple may be safely interpreted as an array or tensor if its elements are actually homogeneous.

(1, 2) : Tuple<Int32, Int32> as Int32[2] as 2xInt32 # Safe

5.4.2. Array

An array is a non-empty statically-sized stack-allocated ordered sequence of homogeneous elements.

As a syntactic sugar, Array<\$tau\$, \$NN\$> \$-=\$ \$tau\$[\$NN\$], e.g. Array<Int32, 2> \$-=\$ Int32[2]. Absense of size (e.g. Int32[]) implies Slice, not array.

An array literal is wrapped in square brackets with traling comma allowed, e.g. [1, 2]. A magic array literal removes the need for commas and allows the underlying-type defining suffix.

[1f, 2f] == %[1 2]f : Float64[2]

An array instance may be safely interpreted as a tensor or a tuple instance, and vice versa if the elements are actually homogeneous.

[1, 2] : Int32[2] as (Int32, Int32) as 2xInt32 # Safe

5.4.3. Tensor

A tensor is a non-empty multi-dimensional array with tensor-specific functionality. A compiler is expected to optimize tensor operations; it also moves number-array-specific features (e.g. \$o.\$) into a separate entity.

A tensor type Tensor<\$tau\$, *\$NN\$> can be shortcut as \$NN\$x\$tau\$, e.g. Tensor<Int32, 2, 3> \$-=\$ 2x3xInt32. There are distinct aliases for vectors (Vector<\$tau\$, size>) and matrices (Matrix<\$tau\$, Rows, Columns>).

Comma-separated tensor literal elements are wrapped in pipes, with each comma-separated row wrapped in square brackets, e.g. |1, 2, 3, 4| \$-=\$ |[1, 2, 3, 4]|. A tensor literal may have an underlying-type-defining suffix. A magic tensor literal removes the need for commas and allows the underlying-type defining suffix.

|1, 2, 3, 4| : Vector<Int32, 4> : 4xInt32

%|[1 2]
  [3 4]|f : Matrix<Float64, 2, 2> : 2x2xFloat64

%|[[1 2][3  4]]
  [[5 6][7 20]]| : Tensor<Int32, 2, 2, 2> : 2x2x2xFloat32

A tensor instance may be safely interpreted as an array or tuple instance and vice versa if the elements are actually homogeneous.

|1, 2, 3, 4| : 4xInt32 as
  Int32[4] as
  (Int32, Int32, Int32, Int32) # Safe

5.5. Variant

In computer science, a tagged union, also called a variant […], is a data structure used to hold a value that could take on several different, but fixed, types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use.

— Wikipedia contributors

A possible variant value is called an option in Onyx. Option type ordering in a variant type is irrelevant, i.e. Variant<\$tau\$, \$upsilon\$> \$-=\$ Variant<\$upsilon\$, \$tau\$>.

A variant constructor accepts either a narrower variant instance or an instance of one of its options type (i.e. may be autocast).

Example 12. Variant construction
final x : Variant<Int32, String> = 42         # OK, accepts an option instance
final y : Variant<Int32, Float64, String> = x # OK, accepts a narrower variant

A variant option of type \$tau\$ may be extracted with either \$tt "@get"\$<\$tau\$> or \$tt "@get?"\$<\$tau\$> instrinsic invocation. In contrary, the \$tt "@neither"\$<*\$tau\$> intrinsic throws if it does currently hold any type from \$tau\$. The safety of invoking an intrinsic depends on the instance storage: a local variant instance has threadsafe access, otherwise it’s fragile.

You can explicitly obtain a variant tag offset in bytes using the \$tt "@offsetof"\$ intrinsic.
Example 13. Variant intrinsics
import rand from "std/rand"

# TIP: The top-level scope is fragile.

final v = rand(42, "foo") : Variant<Int32, String>

final a = @get<Int32>(v) : Int32      # May throw `VariantError` if it's NOT `Int32`
final b = @get?<String>(v) : String?  # May return `nil` if it's NOT `String`
final c = @neither<Int32>(v) : String # May throw `VariantError` if it IS `Int32`

# Unsafely get a `String` option.
final d = unsafe! *((&v as void*)[@offsetof(v)] as String*)

If a result of a check whether a variant currently holds an option of type \$tau\$ is assigned to a local variable \$x\$ defined within a condition or a switcher, then the \$x\$ type is narrowed to \$tau\$. A truthy-ness check applied to a variable defined in a condition also narrows down its type.

final v0 = rand(42, "foo", nil)

if (let v1 = v0) {
  v0 : Variant<Int32, String, nil> # Intact
  v1 : Variant<Int32, String>      # Narrowed down to non-nilable

if (let v2 = @get?<Int32>(v0)) {
  v0 : Variant<Int32, String, nil> # Intact
  v2 : Int32                       # Narrowed down

switch (let v3 = v0) {
  case Int32 {
    v0 : Variant<Int32, String, nil> # Intact
    v3 : Int32                       # Narrowed down
  } else {
    v0 : Variant<Int32, String, nil> # Intact
    v3 : String?                     # Narrowed down (still nilable)

Querying a variant at any scope transparently proxies the query to its actual option. Therefore, all of the options must implement the access for the compilation to succeed.

final v0 = rand([1, 2], "foo", nil)

# v0.size() # => Panic! `nil.size` is undefined

if (let v1 = v0) {
  v1.size() # OK (both `Int32[2].size` and `String.size` are defined)

@neither<nil>(v0).size() # OK, may throw

5.6. Atomic

An Atomic<\$tau\$> type implementation contains a set of static atomic operations accepting a pointer to an object of type \$tau\$. A threadsafe operation overload is performed with sequential consistency, i.e. the strongest memory ordering guarantees. A fragile operation overload accepts an explicit \MemoryOrdering template argument.

let x = Box(42)

threadsafe! {
  fragile! x.value += 1 # Not threadsafe
  Atomic.incr(&x.value) # Increment atomically with sequentially consistent ordering
  fragile! Atomic.incr<:release>(&x.value) # Increment atomically with "release" ordering

There is a number of existing implementations for primitive types, e.g. Atomic<Int32>. A programmer is also free to implement their own Atomic types.

let x = Atomic<Int32>(0)

threadsafe! {
  x += 1 # Calls `Atomic<Int32>.+=`, which increments self atomically
  fragile! x.incr<:release>(1)

Without a MemoryOrdering template argument, a @fence instrinsic invocation emits a sequentially consistent fence. Otherwise, the invocation is fragile.

let x = Box(42)

threadsafe! {
  @fence() # Implicitly `@fence<:seqcst>()`, thus threadsafe

  fragile! @fence<:acquire>()
  fragile! box.value += 1
  fragile! @fence<:release>()

Atomic functions and @fence intrinsic also accept a SyncScope ~ \String template argument, which defines its program-wide synchronization scope.

let x = 42

# These ops are *not* syncronised.

An instance of Atomic<\$tau\$> declares a set of atomic access methods. The Standards defines a list of atomic specializations, e.g. Atomic<Int32>.

let x = Atomic<Int32>(0)
x += 1 # Atomically increment with implicit sequential consistency
x.+=<:acquire, "scope2">(1) # A verbose call

5.7. Lambda

A lambda in Onyx is a class containing a function and a closure instance. A lambda is denoted by the ~> arrow.

A lambda’s closure fields are considered the lambda’s; it’s a class, therefore a field access is always fragile.

A lambda’s function is invoked by calling the lambda instance with the function’s safety. By default, the function safety is inferred from the scope the lambda is being created in. The function body has access to the lambda’s instance variable via \$tt "this"\$.

let x = 0

# The storage modifier can be omitted here
# due the containing scope also being fragile.
let lambda = fragile [{
  # Enclosing a variable pointer is deemeed as moving it to an outer scope, which is unsafe.
  # It may be cast to another pointer storage, which would also be unsafe. The exact flow
  # depends on the application's requirements. If the lambda outlies this function,
  # the pointer is better have undefined storage for unsafe access.
  final ptr = unsafe! &x as Int32*w
}](mod : Int32) ~> {
  *this.ptr += mod

Lambda:Lambda(lambda, 3)

assert(x == (unsafe! *lambda.ptr) ==< 6)

6. Literal

A literal is a hard-coded value, e.g. 42 or "foo".

When used in runtime, a literal creates an instance of inferred or restricted type.

let x = 42           # : Int32
let x = 42f          # : Float64
let x : Float64 = 42 # : Float64

def foo(arg : Float64) -> arg : Float64
assert(foo(42) == foo(42f))

A literal may also be used as a template argument. The template argument may then be used as if it was a real literal. Only \Int, \UInt, \Bool, \String and enum (see Section 4.10.2, “Enum template argument”) may be used as a literal restriction.

struct Foo<L ~ \UInt> {
  def foo() -> L

assert(Foo<42>().foo() == 42 : UInt32)

6.1. Literal suffix

A literal may have a literal suffix to narrow the resulting type. For example, 42 : Float64 is legal, but 42i : Float64 is not: the latter is narrowed to be an integer.

6.2. Magic literal

A magic literal (a term borrowed from Ruby) begins with %.

An array or tensor magic literal only contains literal values, which allows to get rid of commas, and to append an underlying type suffix.

If the very first value of an array magic literal is an alpha character, then it’s deemed to be a String array (it can be enforced with a s suffix). The c suffix would make it an array of Char instead.

%[hello world] : String[2]
%[hello world]c : Char[11]

A magic literal wrapped in parentheses or brackets is deemed to be a single String literal allowing double quotes within.

%("Hello, world") == %{"Hello, world"} |== %<"Hello, world"> : String

7. Variable

An Onyx variable is defined with \$tt "let"\$ directive. For C variables, see Section 12, “Interoperability”.

A variable can be reassgined and mutated. If a variable declared with a \$tt "final"\$ directive, it becomes a final variable. A final variable can not be reassigned. A final record or struct variable is deemed a collection of forcebly-final fields and thus becomes immutable; whereas a final class variable is still mutable, i.e. its fields may be changed.

A variable identifier shall match the following regular expression: /[a-zA-Z_](a-zA-Z0-9_)*/. Otherwise, it is wrapped in backticks; in that case, the identifer may contain any Unicode character except a backtick.

Wrapping UTF-8 identifiers in backticks both for Onyx and C allowed to elegantly solve the problem of spaces in C identifiers, e.g. $`long int`* .

A variable always has concrete type in runtime. It may be restricted to a type upon definition or access using the \$var\$ : \$tau\$ notation. Otherwise the type is inferred, if possible. See Section 4.2, “Type restriction”.

let x = 42
x = 43 # OK

final `ё` : Int32 = 44
# `ё` = 45 # Panic! Can not assign to a final variable

A redefinition of a variable with the same identifer and type is legal.

7.1. Variable storage

A variable has storage. An on-stack variable is local, accessing it is threadsafe. A static variable is defined in the top-level or unit scope, or with an explicit \$tt "static"\$ modifier; accessing it is fragile. A class field has instance storage, and its access is fragile as well.

A pointer to a variable respects its writeability and storage. A class field pointer has undefined storage, because the class instance may eventually die. See Section 5.2, “Pointer”.

7.2. Assignment

A record, struct, enum, variant or function instance is assigned by value, i.e. copied. A class (including lambda) instance is assigned by reference; the reference counter is implicitly incremented upon assignment and decremented upon leaving the scope (see Section 4.9.2, “GC”). A freestanding (i.e. not contained in a variant) unit and \$tt "void"\$ don’t have size and therefore its underlying assignment mechanism is undefined; moreover, \$tt "void"\$ can not be assigned at all.

If an assignment operation has a receiver, its result would be yet another copy of the assigned value.

class Dog {
  let name : String
  static self(name) -> self({ name })

final spike = Dog("Spike")        # Reference is created, RC = 1
final friend = spike              # Reference is copied, RC = 2
final good_boy = (friend = spike) # Another reference is copied, RC = 3

Assigning or passing a literal creates a new object instance with type depending on the literal itself and its restriction.

let x : UInt32 = 42 # Would be `Int32` without the restriction
let y = x           # Copy the `x` value and set `y` to it

A compiler would panic if it detects a use of an undefined yet variable or a variable which hasn’t been assigned yet. A variable may be unsafely assigned an explicitly \$tt "unitialized"\$ value, though. Accessing it would be an undefined behaviour.

# x = 42 # Panic! Undeclared variable `x`

let x = unsafe! uninitialized Int32 # OK

x # Accessing `x` here is undefined behaviour

x = 42
x # OK, it's now safe

7.2.1. Multi-assignment

Multiple variables can be assigned at once, also during definition. When assigning to a value \$alpha\$, x, y = \$alpha\$(x = (y = \$alpha\$)). Therefore, the latest inferred type wins, unless explicitly restricted.

Example 14. Multi-assignment
let x, y = 42

# Equivalent to:

let y = 42
let x = y
let x, final y = 42, z : Float64 = 69

# Equivalent to:

final z = 69 : Float64
final y = 42
let x = y

It is illegal to assign multiple variables to multiple values. Instead, explicit splatting should be used. A record, tuple, array or tensor container can be splatted using the * operator. Upon assigning a splatted container to multiple variables, arities must match. A splatted record also includes its fields' labels.

You may imagine the splat operator as a road roller flattening its operand into the source code.
# These are all equivalent.

let x, y = *(1, 2)
let x, y = *[1, 2]
let x, y = *|1, 2|
let x, y = *{ x: 1, y: 2 }

# let x, y = *(1, 2, 3) # => Panic! Splatting arity mismatch

A variable may be labeled to map a splatted record’s field.

final record = { x: 1, y: 2 }

final x: a, y = *record

# assert(x == 1) # => Panic! Undeclared `x`
assert(a == 1)   # OK
assert(y == 2)   # OK

Pointer dereferencing operation has higher precedence than splatting.

let tuple = (a, b)
let x, y = **&tuple # First, dereference the tuple. Then, splat it

Splatting allows to call a binary operator with multiple arguments. For example, (0.1 + 0.2) ~= *(0.3, delta: 0.1) \$-=\$ (0.1 + 0.2).~=(0.3, delta: 0.1).

7.2.2. Autocasting

There is just a little assignment autocasting in Onyx. Note that assignment is a copy operation, passing an argument is therefore also considered an assignment. Autocasting, on the other hand, is not an operation, but a compiler instruction.

A pointer type may be autocast based on its properties. A variant may be assigned its option type instance. Callables (function, lambda, generator) also have specific autocasting rules. A record, struct, class, unit or enum instance may be implicitly upcast to an ancestor type.

Anything else requires explicit casting with \$tt "as"\$ operator, or conversion of some sort.

let x : Int32 = 42

# let y : Float64 = x   # Panic! Can not assign `Int32` to `Float64`
let y =<Float64>() # OK

class Animal { }
class Dog : Animal { }

final animal : Animal = Dog() # OK

7.3. Field

A variable defined with an (implicit) instance storage within a record, struct or class type is called a field.

A \$tt "final"\$ record or struct variable is deemeed immutable: its fields can not be reassigned. A \$tt "final"\$ class variable, on the other hand, allows updating its fields as long as they are public.

Accessing a field of a local record or struct variable is threadsafe. If the variable is static, the access is fragile.

struct Point {
  let x, y : Float64
  def length() -> (this.x ** 2 + this.y ** 2).sqrt()

let point = Point({ x: 3, y: 4}) # A local struct variable

assert(point.x == 3)                # Threadsafe field access
assert(point.length() ~= *(5, 0.1)) # Threadsafe method access

7.3.1. Default field value

A field may have a default value expression, which is evaluated in a threadsafe context if the field is not explicitly initialized upon the containing object creation. A unit field must always have a default value set.

It is legal to override a field with the same type and any default value, even none. The latest default value expression wins.

struct Point {
  let x, y : Float64 = 0

assert(Point({ x: 0, y: 0 }) == Point())

Within a field default value expression, another field may be accessed via querying \$tt "this"\$ or directly if it is in the same scope.

let a = 42 # Static variable

struct Foo {
  let x = a        # OK, `a` defined in the top level
  let y = x        # OK
  let z = this.y   # Also OK
  # let b = this.b # => Panic! Recursive field access

extend Foo {
  let x = 43     # OK, same type
  # let c = y    # => Panic! Did you mean `this.y`?
  let c = this.y # OK

7.3.2. Accessor

Defining a field with \$tt "let"\$ generates a public getter and a public setter, collectively referenced to as accessors.

Both fragile and threadsafe accessors are implemented for a field, unless a custom getter or setter is implemented.

Within a containing type’s method, \$tt "this"\$ evaluates to an instance of that type. A field lookup is only possible on this, i.e. it can’t be accessed from within a method scope.

struct T {
  let field : Int32 = 0

  def foo() {
    # field         # Panic! Undeclared variable `field`
    this.field      # OK, getter
    this.field = 42 # OK, setter

T().field = 42

A \$tt "final"\$ definition generates a public getter only.

struct T {
  final field : Int32 = 0

  def foo() {
    # this.field = 42 # Panic! Undeclared method `T:field=`

# T().field = 42 # Panic! Ditto

A special \$tt "getter"\$ directive generates a public getter and a private setter.

struct T {
  getter field : Int32 = 0

  def foo() {
    this.field = 42

# T().field = 42 # Panic! `T.field` is private

7.3.3. Getter

A getter counts as a declared function implementation.

trait Foo {
  decl foo() : Int32

struct Bar : Foo {
  let foo : Int32 = 0


struct Baz {
  let foo : Int32 = 0

# Baz().foo() # => Panic! Undeclared ``

7.3.4. Setter

A function with name ending with = is called a setter.

A setter may be called without parentheses, e.g. obj.field = yobj.field=(y).

A \$tt "let"\$ or \$tt "getter"\$ field definition implicitly defines a setter for the field, which may be overriden in a class type only. If a field is defined without a setter (i.e. with \$tt "final"\$), it may still be defined explicitly; it makes little sense for a struct, though, as its \$tt "this"\$ is a read-only copy.

An assignment almost always implies a copy operation in runtime, so it may be treated as a call. Sometimes you want a class to be reactive to its field changes. Therefore it would be handy to be able to override class setters.

class Scheduler {
  # Could've been defined with `let`,
  # but for readability purposes
  # `getter` is preferred here.
  getter workers : USize

  public reimpl workers=(value) {
    # Access the previous value.
    print("Previous value: #{this.workers}")

    # React to the update. Note that `this.workers` value hasn't been updated yet.

    # Explicitly update the value.
    this.workers = value
Special field assignment

A special field assignment obj.field \$AA\$= value, where \$AA\$ is a sequence of operator chars (one of ~%^&*-+) implicitly expands to obj.field = obj.field \$AA\$ value, e.g. x.y += 1x.y = x.y + 1 and it can not be overriden to avoid unexpected behaviour.

Example 15. Special field assignment
struct Foo {
  let bar : Int32

  # If indirect setter overriding was allowed...
  reimpl bar+=(value) {
    super(value * 2)

let foo = Foo({ bar: 0 }) += 1 == 2 # Wut?

7.3.5. Index accessor

An index accessor implements indexed access behaviour, where an entity is accessed not by its static name, but some runtime value. Special assignment syntax sugar is preserved. Index accessors are usually implemented for indexable, e.g. container, types.

Example 16. Index setters
class List<T> {
  decl [](index : SSize) -> T
  decl []=(index : SSize, value : T) -> T

let list = List([1, 2])

assert(list[0] == list.[](0))

list[0] = 3
list.[]=(0, 3)

list[0] += 1
list[0] = list[0] + 1

8. Jump

A jump statement alters the code execution flow based on some condition.

8.1. if

An \$tt "if"\$ statement implements conditional jump. It may have zero or more additional \$tt "elif"\$ branches, and at most one \$tt "else"\$ branch.

An expression is falsey if it evaluates to either \$tt "false"\$, \$tt "nil"\$ or \$tt "void"\$; otherwise it’s truthy. There are builtin boolean algebra operators: \$tt "and"\$, \$tt "or"\$ and \$tt "not"\$ with &&, || and ! counterparts; all return a boolean value.

A branch condition is a runtime expression always wrapped in parentheses. A branch body must be wrapped in brackets unless it’s a single expression. A branch body may be optionally delimeted with \$tt "then"\$ for readability, regardless of brackets; \$tt "else then"\$ is illegal.

let x = 42

if (x > 0) { foo() }
elif (x == 0) then bar()

A variable defined in a condition expression is accessible from within all following branch bodies. If a branch condition expression evaluates to a variant instance, then the actual variant option is compared.

let var = rand(42, "foo") : Variant<Int32, String>

if (final x = @get?<Int32>(var) : Int32?) {
  x : Int32
} else {
  # final x = @get<String>(var) # => Panic! `x` is already declared
  final y = @get<String>(var)   # OK, may throw if variant type is changed

8.2. while

A \$tt "while"\$ statement implements conditional loops. A loop condition is similar to if's. Similarly, a loop body must be wrapped in brackets unless it’s a single expression. Also, a loop body may be optionally delimeted with \$tt "do"\$ for readability.

while (cond?()) do_stuff()
while (cond?()) do { stuff() }

8.3. switch

A \$tt "switch"\$ statement jumps based on an integral switcher value.

A \$tt "case"\$ branch condition is not a runtime expression: it doesn’t use parentheses, multiple conditions are comma-delimeted and optionally wrapped in square brackets instead.

A switcher may therefore be either:

  • An ~ Int value with numeric literal cases;

  • A enum value with enum constant (or symbol) cases;

  • A class or variant instance with actual type cases.

Unlike in C, there is no waterfall in a \$tt "switch"\$ statement. Switching is exaustive, but an \$tt "else"\$ branch is allowed. A \$tt "then"\$ delimeter is optional akin to such in an if statement.

Switching on an integer
let x = rand(1, 2, 3, 4, 5) : Int32

# Different syntaxes to showcase.
switch (x) {
  case 1        foo()
  case [2] then bar()
  case 3, 4
  else {
Switching on a enum
enum Foo {

switch (rand<Foo>()) {
  case :bar
    print("Is Bar")
  case [Foo::Baz]
    print("Is Baz")

When switching on a class or variant instance, the switched value stays intact. A compiler does not narrow the instance type due to possible access complexities. Within a branch body, the switched instance should be cast manually; a compiler may optimize the cast, though.

Switching on a class instance
let animal : Animal = Dog()

switch (animal) {
  case Dog {
    @inspect(animal)          # => Animal (intact)
    # animal~Dog.bark()       # => Panic! Narrowing virtualization is illegal
    @cast<Dog>(animal).bark() # Manual cast, won't throw (may even be optimized out)
    animal = Cat()            # Legal
  case Dog, Cat {
    if (let dog = @cast?<Dog>(animal)) {
Switching on a variant instance
switch (let v = rand(42, "foo")) {
  case Int32 {
    @inspect(v)             # => Variant<Int32, String>
    v = @get<Int32>(v) + 27 # OK, assigning `Int32` is autocast
  case String
    v = @get<String>(v) + "bar"
  # case 42    # => Panic! Can only switch on a variant type, not its value

TODO: Virtual case example.

9. Function

In Onyx, a function may be declared with a \$tt "decl function"\$ statement. A previously declared function may be then implemented using an \$tt "impl function"\$ statement. A function declaration or call always requires parentheses, which allows to omit the \$tt "function"\$ keyword. It is illegal to implement an underclared yet function.

A function name shall match the following regex: /_?[a-zA-Z][a-zA-Z0-9_#]+/ (see Section 9.5, “Function tagging”). Otherwise a Unicode function name may be wrapped in backticks.

# A declaration contains no body.
decl foo()

# impl bar() { } # => Panic! Undeclared `bar`

impl foo() {
  return 42

Both declaration and implementation may be defined simultaneously with a \$tt "def function"\$ statement. Once again, the \$tt "function"\$ keyword is optional.

def foo() {
  return 42

An inline function may be implemented. Instead of a block body, an inline function expects a single expression body, delimeted with ->. Also, function body latest expression value is deemed its return value.

def foo() -> 42

A block body may also be delimeted with ->. In fact, a function prototype should be read as (\$A\$) → \$R\$, where \$A\$ is the function’s arguments, and \$R\$ is its return type. Strictly speaking, a function body is a block or single expression following the function prototype.

9.1. Function prototype

A function may declare zero or more arguments, which defines its arity.

An argument declaration is really a variable declaration with multi-assignment, splatting etc. implicitly declared with \$tt "let"\$. All arguments therefore are required to have their type concrete and known in advance, or inferred. An argument may be declared \$tt "final"\$, which would prohibit its rewriting within the function body.

An argument may have a default value. Such an argument can be omitted in a call, which would assign an evaluated default expression result to the argument. Another call evaluates the default expression once again and so on.

def bind(server : Server, port : UInt16, host: String = "localhost") {
  print("Listening at #{ }:#{ options.port }...")

bind(server, 6969) # -> Listening at localhost:6969...

An argument type may be a record type. A record type allows default values to be set for its fields.

def bind(server : Server, options : { host: String = "localhost", port : UInt16 }) {
  print("Listening at #{ }:#{ options.port }...")

bind(server, { port: 6969 }) # -> Listening at localhost:6969...

A record argument may also have a default value by itself. The actual type may be inferred.

def bind(server : Server, options = { host: "localhost", port: 8000 }) {
  @debug<@typeof(options)>() # => { host : String = "localhost", port : Int32 = 8000 }
  print("Listening at #{ }:#{ options.port }...")

bind(server)                 # -> Listening at localhost:8000...
bind(server, { port: 6969 }) # -> Listening at localhost:6969...

A function always has its return type known in advance, either explicitly set or inferred. A return type is delimeted with ->. A function block body shall follow the return type; otherwise the return type would be deemed the function body.

def sum(a, b : Int32) -> Int32 {
  return a + b

A function may be overloaded, i.e. declared for another set of arguments. In fact, overloading by an argument name is illegal; you may only overload it by its type. Differences in default values presence also counts as an overload. A function may also be overloaded with storage and safety modifiers.

def sum(a, b : Float64) -> Float64 {
  return a + b

# This is an overload.
def sum(a, b : Int32) -> Int32 {
  return a + b

# The function was declared implicitly fragile in the code above.
# This is a safety modifier overload.
threadsafe def sum(a, b : Int32) -> Int32 {
  return a + b

# Storage overload would only work within a type declaration, not in the top level.
# static def sum(a, b : Int32) -> Int32 # => Panic! A top level function can not
                                        # => have a `static` modifer.

A function may be declared generic by declaring an argument or return value of a template type in the function prototype. A generic function specializes once for each unique combination of its template arguments. Delayed macros are evaluated during specialization, and template types are concretized. If an matching implementation already exists, it can be re-implemented using a \$tt "reimpl"\$ statement.

decl sum<T ~ Numeric>(a, b : T) -> T # OK, declarations may repeat

# forall T ~ Float impl sum<T>(a, b : T) -> T # => Panic! Already implemented `sum<Float64>`
forall T ~ Float reimpl sum<T>(a, b : T) -> T { # OK
  \{% print "Specialized #{ `T` }" %}
  return a + b

sum(1, 2)          # No input, because `sum<Int32>` is implemented earlier
sum(1.0, 2.0)      # => Specialized `sum<Float64>`
sum<Float64>(1, 2) # Would not specialize `sum<Float64>` again

A function may also be overloaded by its return type.

def random<T>() : Int32 -> unsafe! $rand() as Int32

def random<T>() : Float64  -> {
  unsafe! (($rand() as $double) / ($RAND_MAX as $double)) as Float64

# let rand = random()           # => Panic! Ambuguity
let rand_f = random() : Float64 # OK
let rand_i : Int32 = random()   # OK

9.1.1. Unsplatting

An argument may be unsplatted by prefixing its name with * during the declaration. The argument would become a single tuple with each element matching the restriction (if any). Also see Section, “Splatting”.

def foo<T>(*arg : T) -> @inspect<@typeof(arg)>()
foo<A>(a)    # => A
foo<A>(a, a) # => (A, A)

def bar<*T>(arg : T) -> @inspect<@typeof(arg)>()
bar<A>(a)            # => A
# bar<A, B>(a, b)    # Panic!
bar<A, B>((a, b))    # => (A, B)

def baz<*T>(*arg : T) -> @inspect<@typeof(arg)>()
baz<A>(a)                 # => A
baz<A>(a, a)              # => (A, A)
baz<A, B>((a, b))         # => (A, B)
baz<A, B>((a, b), (a, b)) # => ((A, B), (A, B))

# Would expand to `arg : A, B`, which doesn't make sense.
# def qux<*T>(arg : *T); # Panic!
# TODO: qux<Int32, String>(42, "Foo")

9.2. Function modifier

A function may be declared with a modifier. When any function modifier is present, a function is implicitly defined, which allows to omit the \$tt "def"\$ keyword, e.g. static self() -> self({ }).

9.2.1. Function storage

A function storage default depends on the declaration scope. A top-level function implicitly has static storage. Any other function declaration would have an instance storage, i.e. be a method (with an exception for a method declared within a unit type: it’d have some interim storage, practically static). To declare an explicitly static function, a \$tt "static"\$ modifier should be used. A function may be overloaded by storage.

class Foo {
  # An explicitly static function.
  static def bar() -> 42

  # An implicitly instance function, i.e. method.
  def bar() -> 43

final foo = Foo()
assert(foo::bar() !=

9.2.2. Function safety

TODO: Can overload by safety, fragile by default.

9.2.3. Function visibility

By default, a declared function is publicly accessible. To declare a function which is only accessible within the declaration scope (e.g. a type implementation), use an explicit \$tt "private"\$ visibitility modifier. A function shall not be overloaded by visibitility; the only exception is the private self constructor implicitly defined for a class. A private function can not be accessed by any means; the only workaround would be to declare a public proxy function.

class Foo {
  private def bar() -> 42

final foo = Foo()

# # Panic! `` is private

# Define a proxy method.
def Foo.get_bar() -> bar()

assert(foo.get_bar() == 42) # OK

9.3. Method

A method is a function member with instance storage.

A method invocation implicitly passes the caller object of type \$tau\$ as the very first argument implicitly declared as \$tt "final"\$ \$tt "this"\$ : \$tau\$. A struct method therefore can not modify the caller, a struct method call safety depends on the caller’s safety. A class method still allows to mutate \$tt "this"\$, but you can not overwrite it completely.

struct Point {
  let x, y : Float64

  def length() {
    # this.x = 42 # => Panic! Can not mutate `this` in a struct method
    return (this.x ** 2 + this.y ** 2).sqrt()

class Balance {
  let value : Float64
  def reset() -> this.value = 0 # OK

To get a method, an instance shall be queried with .. Alternatively, a type may be queried with : for a UFCS variant accepting \$tt "this"\$ argument explicitly.

struct Foo {
  static let bar = 42
  def baz() -> return self::bar

let foo = Foo()

assert(foo.baz() == Foo:baz(foo))

Concretizing a method returns a lambda instance with \$tt "this"\$ being implicitly captured. The UFCS variant returns a Function instance instead. See Section 9.7, “Function object”.

final lambda = foo.baz : Lambda<fragile [{ this: = foo }]() ~> Int32>

# The UFCS variant accepts an object instance explicitly.
final func = Foo:baz : Function<fragile (Foo) -> Int32>

assert(lambda() == func(foo))

A trait method implementation context is not the trait, but a deriving type. Therefore, \$tt "this"\$ within a trait method evaluates to the deriving type object. Ditto for \$tt "self"\$.

9.4. Operator

An operator is a function declaration with identifier containing mathematical symbols.

TODO: Allow Greek and/or Unicode-math symbols?

9.4.1. Unary operator

An unary operator function identifier consists of a single symbol, one of ~^-, and it doesn’t accept any arguments. An unary operator call may be shortcut as \$@\$\$a\$ \$-=\$ \$a\$.\$@\$().

struct Int {
  decl -()

-42 == 42.-()

9.4.2. Binary operator

A binary operator is a function with zero arity and identifier consisting of any sequence of symbols from ~!%^&*-+=.

A binary operator call may be shortcut as \$a\$ \$@\$ \$b\$ \$-=\$ \$a\$.\$@\$(\$b\$).

struct Int {
  decl +(another : self)

1 + 2 == 1.+(2)

A binary operator with identifier ending in = is illegal, with exceptions for <=, >=, == and ~=.

a != b automatically expands to !(a == b). === is the builtin equivalence operator with !== counterpart.

It is possible to pass multiple arguments to a binary operator while preserving the sugar via splatting. For example, (0.1 + 0.2) ~= *(0.3, delta: 0.1) \$-=\$ (0.1 + 0.2).~=(0.3, delta: 0.1).

9.4.3. Ternary operator

The only ternary operators are the ternary if operator (\$cond\$ ? \$a\$ : \$b\$), which can not be overriden; and index setters.

9.5. Function tagging

A function name may also contain one or more #-separated tags. The order of tags doesn’t matter. A Unicode-named function isn’t considered tagged even in the presence of a # character.

def foo() -> {

def foo#prefixed() -> {

def foo#suffixed() -> {

def foo#prefixed#suffixed() -> {

foo#suffixed#prefixed() # -> Prefix␤42␤Suffix

# This function isn't tagged.
def `Функция#tag` -> { }

9.6. Generator

A function may also accept a generator argument, which defines a block of code to generate upon every invocation. The generated code is injected on the caller site, which allows to safely reference outer-scope identifiers.

A single-argument-yielding generator invocation allows to omit parentheses upon the yield.

class List<T> {
  # A function accepting a generator argument called *yield*.
  # The generator yields a single value of type *T*,
  # the generated block return value is discarded.
  # The function itself returns void.
  def each(yield : (T) => discard) : void -> {
    (0..this.size).each(i => yield(this[i]))

final console = GetConsole()

# list.each((e : T) => discard { console.print(e) }) # Too long
list.each(e => console.print(e))                     # That's better

# Generates code similar to this, with visibility taken care of:
(0..list.size).each(i => console.print(list[i]))

9.7. Function object

When querying a function specialization, a Function object is returned. A function object has undefined memory layout, but it certainly can be used in runtime, passed by value and having program-wide lifespan. A function object may be called at some later point of time.

def sum(a, b : Int32) -> return a + b
final sum_obj = sum : Function<fragile (a : Int32, b : Int32) -> Int32>
assert(sum_obj(1, 2) == 3)

ALternatively, a function object may be defined in-place.

let sum = (a, b : Int32) -> a + b
sum(1, 2)
assert(sum(1, 2) == 3)

A single-argument in-place function declaration may have argument parentheses omitted. This also applies to a generator invocation and lambda declaration.

let func = x -> x * 2
generator(x => x * 2)       # Can not assign a generator, only pass it
let lambda = x ~> x * 2     # OK if doesn't have closure
# let lambda = []x ~> x * 2 # What?

9.8. Out-of-scope declaration

A function may be declared out of the type implementation scope. An actor (i.e. the type the function is declared for) may be a type expression, which would only apply the statement if the caller matches the expression.

Example 17. Out-of-scope declaration
class List<T> : Enumerable<T>, Indexable<Key: USize, Value: T> {
  impl ~Enumerable.each(yield) -> {
    (0..this.size).each(i => yield(this[i]))

# When a type matches both `Enumerable<V>` and `Indexable<K, V>`,
# declare a new method `each#indexed` for it.
forall K, V decl (
  Enumerable<V> && Indexable<K, V>
).each#indexed(yield : (K, V) => discard) -> void

# Implement the freshly declared method for a `List` instance.
forall T impl List.each#indexed(yield) -> {
  (0..this.size).each(i => yield(this[i], i))

10. Exception handling

An Onyx program may \$tt "throw"\$ an exception in runtime.

A potentially throwing region of code may be wrapped in a \$tt "try"\$ statement, which may contain zero or more \$tt "catch"\$ clauses. A \$tt "catch"\$ clause accepts a generator block with a single argument, which is the caught exception instance.

Example 18. Basic exception handling
try {
  throw Exception("Boom!")
} catch ((e) => {
  print("Caught exception: #{ e.message }")
$ fnx main.nx
Caught exception: Boom!
$ echo($?)

A \$tt "catch"\$ clause may be overloaded to be triggered on a matching exception type. To make an object throwable, it must be or inherit the builtin Exception class. A String instance may also be thrown; in that case, the string is implicitly passed as the message argument, i.e. throw "Message" \$-=\$ throw Exception("Message").

Example 19. Custom exception class
class Boom : Exception {
  static self() -> self({ Exception("Boom!") })

try {
  throw Boom()

# Would match anything other than `: Boom`.
catch (e => {
  print("Uncaught exception: #{ @typeof(e) }")

# Would match `: Boom` only.
catch ((e : Boom) => {
  print("Explosion averted")
$ fnx main.nx
Explosion averted
$ echo($?)

An Onyx program contains the implicit default last-resort handler which would pretty-print an unhandled exception and gracefully exit the program. A compiler preserves macro data to ease debugging.

Example 20. Rescuing unhandled exception
  %Q[def foo() -> {\nthrow "Oops"\n}]

$ fnx main.nx
Exception! Oops

@ <macro>:2:3

  1. | def foo() -> {
  2. |   throw "Oops"
  3. | }

  % /main.nx:1:1

    1. | {{
    2. |   %Q[def foo() -> {\nthrow "Oops"\n}]

@ /main.nx:5:1

  4. |
  5. | foo()

$ echo($?)

11. Macros

In Onyx, a macro is code evaluated during program compilation. Currently, macros are written in embedded Ruby.

There are two categories of macros:

  • Emitting and non-emitting;

  • Immediate and postponed.

An emitting macro emits its result into source code, it’s wrapped in {{ }}. A non-emitting macro doesn’t, it is wrapped in {% %}. An immediate macro is evaluated as soon as it’s encountered in source code. A postponed macro is evaluated at some later point of compilation, usually a specialization; it looks like it’s escaped, e.g. \{% %}.

Example 21. Basic macros
{% %w[hello world].each do |s| %} # A non-emitting immediate macro
  print({{ s.stringify }})        # An emitting immediate macro
{% end %}                         # A non-emitting immediate macro again

After macro evaluation the code would look like this:

 # A non-emitting macro
  print("hello")        # An emitting macro
  print("world")        # An emitting macro
                         # A non-emitting immediate macro again

Macros include API to access and manipulate the AST and the process of compilation. By default, the macros context is isolated, and there is limited access to the system. A compiler may provide a way to enable grained permissions, e.g. network access to a certain domain. See Appendix B, Macros API.

Example 22. Pinging from a macro
print(" is " + {%
    print("Ping success")     # Print during compilation
    nx.emit(%Q["reacheable"]) # Emits source code
    print("Ping failure")
$ fnx main.nx
Ping success
$ ./main is reacheable

12. Interoperability

Onyx features easy two-way interoperation with C code.

12.1. Calling C code from Onyx

A C code may be embedded into an Onyx source file using the extern directive. A single C expression or an entire block of C code shall follow the keyword.

The C code would be parsed, resulting in C declarations accessible from within the containing Onyx source file. See Section 3.3, “C scope”.

Example 23. Calling C code from Onyx
extern #include "stdio.h"
final message = "Hello, world!"
unsafe! $puts(message.pointer as char*)

The Fancy Onyx compiler implicitly links libc and libm, and also automatically looks up for the standard C header directories. Example 23, “Calling C code from Onyx” would therefore have been compiled simply with:

$ fnx main.nx
$ ./main
Hello, world!

The example above doesn’t require to include the whole header file, however. An explicit C function declaration would be enough.

extern void puts(char*);
final message = "Hello, world!"
unsafe! $puts(message.pointer as char*)

12.2. Calling Onyx code from C

It is possible to call Onyx code from C by doing the exact same thing: prepending $ to Onyx entities, also wrapped in backticks if needed. The entities are implicitly mangled to make the interop work seamlessly. Even type inferring works as expected.

Example 24. Calling Onyx code from C
def sum<T>(a, b : T) -> a + b

extern {
  #include "stdio.h"

  void main() {
    printf("1 + 2 = \d\n", $sum(1, 2));

Would compile to machine code similar to this:

declare void @printf(i8*, ...)

; The actual function name would be mangled.
define i32 @"__nx::sum<$int>(a:$int,b:$int):$int"(i32 %0, i32 %1) {
  ; Here goes the generated Onyx code.

define void main() {
  %0 = call i32 @"__nx::sum<$int>(a:$int,b:$int):$int"(1, 2)
  call @printf("1 + 2 = \d\n", %0)

12.3. Exporting a C library

The Standard declares that an Onyx compiler shall be able to compile a freestanding C library containing all the code from the extern directives contained in Onyx source files referenced by the input file. A amalgamation header file may also be compiled in a similar fashion. It would replace function definitions in extern directives with declarations.

Example 25. Exporting a C library
extern {
  #include "stdio.h"

  // This function is implemented in Onyx.
  void print_sum(int a, int b) {
    printf("\d + \d = \d\n", a, b, $sum(a, b));

# The Onyx implementation.
def sum(a, b : $int) -> a + b
# Compile the library file
$ fnx lib ./main.nx -o./onyx.a

# Compile the header file
$ fnx header ./main.nx -o./onyx.h

This creates two files: library onyx.a and header onyx.h. They may be then used in a pure C environment.

#include "stdio.h"

// This function is implemented in Onyx.
void print_sum(int a, int b);
#include "./onyx.h"

int main() {
  print_sum(1, 2);
$ cc onyx.a main.c -o./main
$ ./main
$ 1 + 2 = 3

Appendix A: Intrinsics

builtin threadsafe @fence<
  SyncScope ~ \String = undefined

builtin fragile @fence<
  Ordering : \MemoryOrdering = :seqcst,
  SyncScope ~ \String = undefined

Appendix B: Macros API

This section will contain the macros API.

Appendix C: Standard library

The language Standard defines the standard library: a collection of types and functions for cross-platform functionality required for a conforming compiler implementation. For brewity, the standard library is often referenced to as stdlib. An stdlib entity must be imported from "std".

The appendix contains the cross-platform standard library API. It is WIP and is soon to be a part of the Standard rather than the Reference.

C.1. Containers

List, Stack, Queue, Deque, Map, Set

C.2. Hashing


C.6. Target-specific libraries

If a compiler claims to be able to target a specific platform, it then shall implement target-specific APIs accessible under a "target/*" meta path, e.g. "target/win32".

C.6.1. ISA

A conforming compiler implementation targeting certain Instruction Set Architectures must implement ISA-specific set of standardized APIs at "std/isa/*", e.g. "std/x86" if it supports X86 processors.


x86-specific APIs are located in "std/isa/x86".

C.6.2. OS

A conforming compiler implementation targeting certain operating systems must implement OS-specific set of standardized APIs at "std/os/*", e.g. "std/win32" if it supports Windows programs.


POSIX-specific APIs are located in "std/os/posix".

To avoid bloat, a POSIX-compilant OS API doesn’t expose POSIX functionality, but only a distinct set of features for that OS. For example, "std/os/linux" won’t expose Signal, as it’s already located in "std/os/posix" when targeting Linux.

# import { Signal } from "std/os/linux" # => Panic!
import { Signal } from "std/os/posix"   # OK, POSIX functionality implemented for this target
import { IOUring } from "std/os/linux"  # OK, Linux-specific functionality

Linux-specific APIs are located in "std/os/linux".

"std/os/linux" doesn’t expose POSIX APIs. See Section C.6.2.1, “POSIX”.

Win32-specific APIs are located in "std/os/win32".

Appendix D: Panic

This soon-to-be-standardized appendix lists panic codes with their descriptions.