## Foreword

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 `Int2^32` notation to denote a binary integer with infinite-scalable radix. Until then, stay fast and practical.

### Reference conventions

Compiler

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 https://github.com/fancysofthq/onyx-ref.

## 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
sum.nx
``````export default def sum(a, b : Int32) {
return a + b
}``````
main.nx
``````import { out } from "std/term"
import sum from "./sum.nx"
out << sum(1, 2)``````
``````fnx ./main.nx 3`````` #### 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 user.nx ``````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] } }`````` post.nx ``````import User from "./user.nx" export default class Post { final author : User # ^ Jump point let rating : UInt32 def author_name() -> { 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. ## 2. Comments 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. @[SomeAnnotation] 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() assert( 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 = this.bar + 27 # Also OK def qux() { # bar # => Panic! Undeclared variable `bar`, did you mean `this.bar`? this.bar # 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.

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.

• 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 `Foo.bar```````

### 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 `include`s) are accessible anywhere. For example, `$int`, `$char*` etc.

}
}``````

#### 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*)

@incrc(db)
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 {
Bar,
Baz
}

enum NotFoo {
Bar,
Qux
}

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 {
Red,
Green,
Blue
}

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 {
Write
}

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

}

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

# Implement for both readable and writeable.
decl common()
}

### 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
}
}

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() -> this.foo()
static def baz() -> "qux"
}

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

assert(Unit.bar() == 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
}

assert(nil::antipattern?)``````

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

@[MyAnnotation<"foo">]
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"].annotations.map(&.type.name).includes?("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 https://en.wikipedia.org/wiki/Real_number 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` • `Int*` — a signed binary integer with variable bitsize; • `UInt*` — an unsigned binary integer with variable bitsize; • `Bit` • `Byte` — alias to `UInt8`; ###### `Float` • `Float*` — an IEEE 754 binary floating point number with variable bitsize; ###### `Fixed` `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 = 42``T: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 `"local"` N/A Unsafe Unsafe Threadsafe No Threadsafe `"instance"` Unsafe N/A Unsafe Threadsafe No Fragile `"static"` Unsafe Threadsafe (1) N/A Threadsafe Yes Fragile `"undefined"` Unsafe Unsafe Unsafe N/A Yes Unsafe (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 `NNxtau`, 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
https://en.wikipedia.org/wiki/Tagged_union

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)

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)

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)

@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.
Atomic.incr<"scope1">(&x)
Atomic.incr<"scope2">(&x)``````

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(1)
lambda.Lambda(2)
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`````` ##### Splatting 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 = x.to<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 T().field = 42`````` A tt "final" definition generates a public getter only. ``````struct T { final field : Int32 = 0 def foo() { this.field # this.field = 42 # Panic! Undeclared method `T:field=` } } 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 this.field = 42 } } T().field # 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 } Bar().foo Bar().foo() Bar()~Foo.foo() struct Baz { let foo : Int32 = 0 } Baz().foo # Baz().foo() # => Panic! Undeclared `Baz.foo()``````` #### 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 = y``obj.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. internal_resize(value) # 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 += 1``x.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 }) foo.bar += 1 foo.bar == 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() else baz()`````` 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 baz() else { qux() } }`````` Switching on a enum ``````enum Foo { Bar, Baz } 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)) { dog.bark() } } }`````` 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.host }:#{ 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.host }:#{ 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.host }:#{ 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 7.2.1.1, “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() != 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()

# foo.bar() # Panic! `Foo.bar` 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() -> {
print(42)
}

def foo#prefixed() -> {
print("Prefix")
foo()
}

def foo#suffixed() -> {
foo()
print("Suffix")
}

def foo#prefixed#suffixed() -> {
foo#prefixed()
print("Suffix")
}

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
main.nx
``````try {
throw Exception("Boom!")
} catch ((e) => {
print("Caught exception: #{ e.message }")
})``````
``````$fnx main.nx Caught exception: Boom!$ echo($?) 0`````` 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 main.nx ``````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($?)
0``````

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
main.nx
``````{{
%Q[def foo() -> {\nthrow "Oops"\n}]
}}

foo()``````
``````$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($?) 1`````` ## 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("example.com is " + {% if Net::Ping::External.new("example.com").ping? print("Ping success") # Print during compilation nx.emit(%Q["reacheable"]) # Emits source code else print("Ping failure") nx.emit(%Q["unreacheable"]) end %})`````` ``````$ fnx main.nx -m.net=example.com
Ping success
$./main example.com 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 main.nx ``````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

$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. onyx.h ``````#include "stdio.h" // This function is implemented in Onyx. void print_sum(int a, int b);`````` main.c ``````#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

XXH, SHA, MD

### 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

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

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

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

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

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

## Appendix D: Panic

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