Introduction

Welcome to the Dyon 0.39 tutorial!

Dyon is a dynamically typed scripting language, designed for game engines and interactive applications. It was started in 2016 by Sven Nilsen.

This tutorial is written for people with little programming experience.

The language in this tutorial is made simple on purpose. If you are experienced programmer, you might want to skip to the parts that interests you.

Dyon is different!

If you have used another programming language before, there are some things worth to keep in mind:

Dyon has a limited memory model because of the lack of a garbage collector. The language is designed to work around this limitation.

The language takes ideas from Javascript, Go and Rust, but focuses on practical features for making games:

  • Optional type system with ad-hoc types
  • Similar object model to Javascript, but without null
  • Built-in support for 4D vectors, HTML hex colors
  • Go-like coroutines

There are plenty of new ideas in Dyon:

  • Lifetime check for function arguments
  • Use return as local variable
  • Mathematical loops
  • Current objects
  • Secrets
  • Grab expressions
  • Dynamic modules as a way of organizing code

How this tutorial is organized

At the bottom there is a "Comments" section. This contains things you might have noticed and things you can try out on your own.

Source code

You will find the source code for all examples under "examples" in the git repository.

Getting started

Before you can run Dyon programs, you need to do the following:

  1. Install Rust.
  2. Open up the Terminal window
  3. Type cargo new --bin <project name>

Open up "Cargo.toml" and add the following:

[dependencies]
dyon = "0.36.0"

Open up "src/main.rs" and type:

extern crate dyon;

use dyon::{error, run};

fn main() {
    error(run("src/main.dyon"));
}

Hello World example

Source code: examples/hello_world

Put this in "src/main.dyon":

fn main() {
    println("Hello world!")
}

In the terminal window, go to the "hello_world" folder and type:

$ cargo run

This should print out:

Hello world!

Comments

You might have noticed:

  • Dyon has no ";" at the end of lines
  • The "fn" keyword is used by both Rust and Dyon
  • The "main" function is called when running the program
  • Double quotes "" are used for text

Some things you can try:

  1. Put your name inside the text!
  2. Add a new line where you print out something else!
  3. Change "println" to "print" and see what happens!

Comments

A comment is text to explain how some code works. Comments are ignored when running the program.

Dyon uses // for single-line and /* */ for multi-line comments.

/*

This text is inside a multi-line comment.
It is ignored when running the program.

*/

fn main() {
    // Can I ask you something?
    println("hello?")
}

A single-line comment ignores the rest of the line:

#![allow(unused)]
fn main() {
println("hello") // Prints `hello`.
}

A multi-line comment starts with /* and ends with */.

#![allow(unused)]
fn main() {
/* testing, testing! */
println("hello")
}

You can nest /* */:

#![allow(unused)]
fn main() {
/*
    /*
    A comment inside a comment!
    */
*/
}

Tips and tricks

It is more common to use // than /* */ for documenting the code.

End a comment with a dot to make it easier to see where the line is ending:

#![allow(unused)]
fn main() {
// Alice opened the door
// by pressing a button.
}

You can use /* */ to ignore some code without removing it:

/*
fn main() {
    println("one")
}
*/

fn main() {
    println("two")
}

One technique that helps making code more understandable: Organize the code in paragraphs, like in a book. Write a single-line comment for each paragraph. Separate paragraphs with an empty line.

fn main() {
    // Print the numbers from 1 to 10.
    list := sift i 10 { i + 1 }
    println(list)

    // Print the numbers from 11 to 20.
    list := sift i [10, 20) { i + 1 }
    println(list)
}

Functions

In Dyon, there are two ways to declare a function.

One way is to use fn to define a function:

#![allow(unused)]
fn main() {
fn f(x) -> {
    return x + 1
}
}

If a function does not return a value, you leave out ->:

#![allow(unused)]
fn main() {
fn say_hi() {
    println("hi!")
}
}

Another way is to use mathematical notation:

#![allow(unused)]
fn main() {
f(x) = x + 1
}

All mathematically declared functions returns a value.

Pro tip: To declare constants, use mathematical notation:

#![allow(unused)]
fn main() {
// Speed of light.
c() = 299_792_458
}

Fibonacci example

Source code: examples/fibonacci

fib(x) = if x <= 0 { 0 }
         else if x == 1 { 1 }
         else { fib(x-1) + fib(x-2) }

fn main() {
    for i 20 { println(fib(i)) }
}

Do not worry if you do not understand the code above. This is just to show how to declare a function and call it.

Mutability

In Dyon, when a function changes an argument, you need to add mut:

fn foo(mut x) {
    x = 3
}

fn main() {
    a := 2
    foo(mut a)
    println(a) // prints `3`
}

A function that calls a function that changes an argument, also need mut:

fn foo(mut x) {
    x = 3
}

// Requires `mut x` because it calls `foo`.
fn bar(mut x) {
    foo(mut x)
}

fn main() {
    a := 2
    bar(mut a)
    println(a) // prints `3`
}

This helps programmers understand the code.

Mutability information is part of the function name. You can declare multiple functions with different mutability patterns:

// `name`
name(person: {}) = clone(person.name)

// `name(mut,_)`
fn name(mut person, name) {
    person.name = clone(name)
}

fn main() {
    character := {name: "Homer Simpson"}
    println(name(character)) // prints `Homer Simpson`
    name(mut character, "Marge Simpson")
    println(name(character)) // prints `Marge Simpson`
}

Named arguments

In Dyon, you can call some functions in two ways:

#![allow(unused)]
fn main() {
attack__player_enemy(mut player, enemy)
attack(player: mut player, enemy: enemy)
}

The named arguments are part of the function name.

  • Double underscores separates function name from arguments
  • Arguments are separated by a single underscore

It is common to use named arguments when there are lots of parameters.

Return

In Dyon, return can be used as a variable:

#![allow(unused)]
fn main() {
fn foo(mut a: f64) -> {
    return = a + 2
    a += 2
}
}

If you leave out =, the function will exit with the value:

#![allow(unused)]
fn main() {
fn foo(a: f64) -> {
    return a + 2
    println("hi") // never gets called.
}
}

Functions without -> can use return without a value:

#![allow(unused)]
fn main() {
fn foo() {
    return
}
}

All functions that returns a value must use return. You get an error if you forget it:

#![allow(unused)]
fn main() {
fn foo(a: f64) -> { a + 2 } // ERROR
}
--- ERROR ---
In `source/test.dyon`:

Type mismatch (#775):
Expected `any`, found `void`
1,1: fn foo(a: f64) -> { a + 2 }
1,1: ^

Blocks

A block starts with { and ends with }:

#![allow(unused)]
fn main() {
a := {
    println("hi!")
    5
}
println(a) // prints `5`
}

The last expression in a block becomes the result of block expression.

When declaring a variable inside a block, you might get an error:

#![allow(unused)]
fn main() {
a := {
    b := 5
    b // ERROR
}
}
--- ERROR ---
In `source/test.dyon`:

`b` does not live long enough
2,10:     a := {
2,10:          ^
3,1:         b := 5
4,1:         b // ERROR
5,1:     }

Dyon has no garbage collector, but uses a lifetime checker instead. This is explained later in the chapter "Lifetimes".

This means that "b" only lives for the scope of the block, and you must use clone to fix it:

#![allow(unused)]
fn main() {
a := {
    b := 5
    clone(b) // OK
}
}

When you do a math operation, Dyon knows it is safe:

#![allow(unused)]
fn main() {
a := {
    b := 5
    b + 2 // OK
}
}

Variables

#![allow(unused)]
fn main() {
a := 3 // Create a variable "a" with value `3`.
a = 4 // Change the value of variable "a" to `4`.
}

A variable has a name, a type and a value.

  • The operator := creates a variable.
  • The operator = changes the value of a variable.

When changing the value of a variable, the type must be the same:

#![allow(unused)]
fn main() {
a := 3
a = "hi!" // ERROR
}
--- ERROR ---
main (source/test.dyon)

Expected assigning to text
3,5:     a = "hi!" // ERROR
3,5:     ^

Echo example

Source code: examples/echo

Create a new project and name it "echo".

Put this in "src/main.dyon":

fn main() {
    print("Type something: ")
    line := read_line()
    sleep(1)
    println(line)
}

This program prints out the text you gave it after 1 second. The text is stored inside a variable "line".

Comments

You might have noticed:

  • println ends with a new line, while print stays at same line

Some things you can try:

  1. Print out "You said: (line)"!
  2. Change sleep(1) to wait 5 seconds!
  3. Change the name of "line" to something else!
  4. Put "println(line)" first and see what happens!

Numbers

A number in Dyon is a variable that stores floats with 64 bit precision. The type is f64.

#![allow(unused)]
fn main() {
a := 1
b := .5
c := 2.5
d := -3
e := 1_000_000
f := 1e6
g := -7_034.52e-22
}

You can add, subtract, multiply, divide, take the power and find the division reminder:

#![allow(unused)]
fn main() {
a := 2
b := a + 3
c := a - 3
d := a * 3
e := a^3
f := a / 3
g := a % 3
}

Relative change

The following operators change the value relatively:

#![allow(unused)]
fn main() {
a += 1 // increase by 1
a -= 1 // declare by 1
a *= 2 // multiply by 2
a /= 2 // divide by 2
a %= 2 // find division reminder of 2
}

Comments

Some thing you might have noticed:

  • f64 is used by both Rust and Dyon
  • "One million" can be written as 1_000_000 or 1e6

Strings

A string in Dyon is a variable that stores UTF-8 bytes. This represent text. The type is str.

Use double quotes to write a string:

#![allow(unused)]
fn main() {
a := "hi!"
}

The \ symbol escapes special characters:

  • \n - New line
  • \t - Tab
  • \" - The " character
  • \\ - The \ character
  • \r - Moves to beginning of line without creating a new line
  • \u005c - Unicode character by 4 hexadecimal code

The + operator joins two strings together:

#![allow(unused)]
fn main() {
name := "Santa"
title := "hi " + name
}

The str function converts a value into a string:

#![allow(unused)]
fn main() {
age := 58
println("Bilbo is " + str(age) + " years old!")
}

Some useful functions for strings

  • fn str(any) -> str - convert a variable to a string
  • fn read_line() -> str - read line from standard input
  • fn trim(str) -> str - trims away whitespace at both sides
  • fn trim_left(str) -> str - trims away whitespace at left side
  • fn trim_right(str) -> str - trims away whitespace at right side
  • fn json_string(str) -> str - creates a JSON string
  • fn str__color(vec4) -> str - HTML hex color string
  • fn chars(str) -> [str] - characters of a string

Booleans

A boolean in Dyon is a variable that stores true or false. This represents a choice with two options. The type is bool.

Lazy and eager

A lazy operator does not compute the right argument if the left argument gives the result.

An eager operator computes both arguments.

Logical gates

A logical gate is an operation on two boolean values.

ABA AND BA OR BA XOR BA EXC B
falsefalsefalsefalsefalsefalse
falsetruefalsetruetruefalse
truefalsefalsetruetruetrue
truetruetruetruefalsefalse

There are 4 operators for AND:

#![allow(unused)]
fn main() {
a && b // lazy
a and b // eager
a ∧ b // eager
a * b // eager
}

There are 4 operators for OR:

#![allow(unused)]
fn main() {
a || b // lazy
a or b // eager
a ∨ b // eager
a + b // eager
}

There is 1 operator for XOR:

#![allow(unused)]
fn main() {
a ^ b
}

There is 1 operator for EXC:

#![allow(unused)]
fn main() {
a - b
}

There are 2 operators for NOT:

#![allow(unused)]
fn main() {
!a
¬a
}

Why more than one way

Boolean algebra is used with different notations:

  • Programming applications
  • Logic
  • Non-invertible sets

When programming applications, you use &&, || and !.

Logic use , and ¬.

Non-invertible sets uses *, + and -.

In the future Dyon might get more features for Boolean algebra.

4D vectors

In Dyon, you can compute with 4D vectors:

(x, y, z, w)

Many operations are built-in for working with 4D vectors:

#![allow(unused)]
fn main() {
a := (1, 2, 3, 4)
b := (5, 6, 7, 8)
println(a + b) // prints `(6, 8, 10, 12)`
}

The components z and w are set to 0 by default:

#![allow(unused)]
fn main() {
a := (1, 2)
}

The y component can also be set to 0, but requires ",":

#![allow(unused)]
fn main() {
a := (1,)
}

HTML hex colors

A HTML hex color is converted into a 4D vector:

a := #ff0000 // red
b := #00ff00 // green
c := #0000ff // blue
d := #00000033 // semi-transparent black

Swizzle components

To swizzle components, you can use this notation:

#![allow(unused)]
fn main() {
a := (1, 2)
b := (yx a,)
}

You can repeat a component up to 4 times:

#![allow(unused)]
fn main() {
a := (1, 2)
b := (yyyy a,)
println(b) // prints `(2, 2, 2, 2)`
}

Calling functions

When calling a function, you can unpack vector components:

add(x, y) = x + y

fn main() {
    a := (1, 2)
    println(add(xy a)) // prints `3`
}

If a function takes named arguments, you can use this trick:

add__x_y(x, y) = x + y

fn main() {
    a := (1, 2)
    println(add(x_y: xy a)) // prints `3`
}

Addition and multiplication

Addition and multiplication is per component for two vectors.

#![allow(unused)]
fn main() {
(1, 2) + (3, 4) // `(4, 6)`
(1, 2) * (3, 4) // `(3, 8)`
}

Scalar addition, multiplication and division is allowed on both sides and is per component.

#![allow(unused)]
fn main() {
(1, 2) + 1 // `(2, 3, 1, 1)`
3 + (1, 2) // `(4, 5, 3, 3)`
(1, 2) * 2 // `(2, 4)`
2 * (1, 2) // `(2, 4)`
(1, 2) / 2 // `(0.5, 1)`
2 / (1, 2, 4, 8) // `(2, 1, 0.5, 0.25)`
}

Dot product

Dot product of two vectors can be written in two ways:

#![allow(unused)]
fn main() {
a := (1, 2)
b := (3, 4)
println(a *. b)
println(a · b)
}

Cross product

Cross product of two vectors can be written in two ways:

#![allow(unused)]
fn main() {
a := (1, 2)
b := (3, 4)
println(a x b)
println(a ⨯ b)
}

Norm

The norm of a vector, also called "length" or "magnitude":

#![allow(unused)]
fn main() {
a := (3, 4)
println(|a|)
}

Un-loops

The vec4, vec3, vec2 are un-rolled and replaces the index with a number:

fn main() {
    a := vec4 i { i + 1 }
    println(a) // prints `(1, 2, 3, 4)`
}

You can check this by printing out a closure:

fn main() {
    a := \() = vec4 i { i + 1 }
    // prints `\() = ({ 0 + 1 }, { 1 + 1 }, { 2 + 1 }, { 3 + 1 })`
    println(a)
}

Other functions for 4D vectors:

  • fn x(vec4) -> f64 - get x component
  • fn y(vec4) -> f64 - get y component
  • fn z(vec4) -> f64 - get z component
  • fn w(vec4) -> f64 - get w component
  • fn s(vec4, f64) -> f64 - get vector component by index
  • fn dir__angle(f64) -> vec4 - rotation vector around Z axis

Precision

Whenever you do calculations with 4D vectors, you get float 32 bit precision.

4D vectors

In Dyon, you can compute with 4D matrices:

mat4 {
  m00, m01, m02, m03;
  m10, m11, m12, m13;
  m20, m21, m22, m23;
  m30, m31, m32, m33;
}

Many operations are built-in for working with 4D matrices:

fn main() {
    a := mat4 {1,;}
    b := mat4 {1,;}
    // prints `mat4 {2,0,0,0; 0,2,0,0; 0,0,2,0; 0,0,0,2}`
    println(a + b)
}

4D vectors used as rows:

mat4 {(1, 0, 0, 0); (0, 1, 0, 0); (0, 0, 1, 0); (0, 0, 0, 1)}

Omitted parentheses:

mat4 {1,0,0,0; 0,1,0,0; 0,0,1,0; 0,0,0,1}

Omitted trailing zeroes:

mat4 {1,; 0,1; 0,0,1; 0,0,0,1}

Fills out rows corresponding to identity matrix:

mat4 {1,; 0,1; 0,0,1}

Identity matrix:

mat4 {1,;}

Addition and multiplication

Addition is per component for two matrices.

Multiplication is matrix multiplication.

fn main() {
    a := mat4 {1,;}
    b := a + a
    // Prints `mat4 {2,0,0,0; 0,2,0,0; 0,0,2,0; 0,0,0,2}`.
    println(b)

    // Prints `mat4 {4,0,0,0; 0,4,0,0; 0,0,4,0; 0,0,0,4}`.
    c := b * b
    println(c)
}

+=, -= and *=:

fn main() {
    a := mat4 {1,;}
    a += a
    // Prints `mat4 {2,0,0,0; 0,2,0,0; 0,0,2,0; 0,0,0,2}`.
    println(a)

    // Prints `mat4 {4,0,0,0; 0,4,0,0; 0,0,4,0; 0,0,0,4}`.
    a *= a
    println(a)

    // Prints `mat4 {3,0,0,0; 0,3,0,0; 0,0,3,0; 0,0,0,3}`
    a -= mat4 {1,;}
    println(a)
}

+, - and * with scalars and matrices is per component:

fn main() {
    a := mat4 {1,2,3,4;2,3,4,1;3,4,1,2;4,1,2,3}
    println(2 * a)
    println(a * 2)
    println(2 + a)
    println(a + 2)
    println(2 - a)
    println(a - 2)
}

Transform 4D vector by multiplying a 4D matrix with a vec4 that has a zero in the 4th component:

fn main() {
    // Scale x-axis up 2 times.
    a := mat4 {2,;}
    println(a * (1, 1, 1))

    // Scale y-axis up 2 times.
    a := mat4 {1,; 0,2}
    println(a * (1, 1, 1))

    // Scale z-axis up 2 times.
    a := mat4 {1,; 0,1; 0,0,2}
    println(a * (1, 1, 1))
    // The same using `scale`.
    println(scale((1, 1, 2)) * (1, 1, 1))
}

Transform a point by multiplying a 4D matrix with a vec4 that has a one in the 4th component:

fn main() {
    pos := (1, 2, 3)
    // Put `1` in the 4th component to transform a point.
    println(mov((1, 2)) * (xyz pos, 1))
}

Get row vectors with rx, ry, rz, rw, rv and get column vectors with cx, cy, cz, cw, cv:

fn main() {
    a := mat4 {
        1,2,3,4;
        5,6,7,8;
        9,10,11,12;
        13,14,15,16;
    }

    // Print row vectors.
    println(rx(a)) // Prints `(1, 2, 3, 4)`.
    println(ry(a)) // Prints `(5, 6, 7, 8)`.
    println(rz(a)) // Prints `(9, 10, 11, 12)`.
    println(rw(a)) // Prints `(13, 14, 15, 16)`.

    // Print row vectors using a loop.
    for i 4 {println(rv(a, i))}

    // Print column vectors.
    println(cx(a)) // Prints `(1, 5, 9, 13)`
    println(cy(a)) // Prints `(2, 6, 10, 14)`
    println(cz(a)) // Prints `(3, 7, 11, 15)`
    println(cw(a)) // Prints `(4, 8, 12, 16)`

    // Print column vectors using a loop.
    for i 4 {println(cv(a, i))}
}

Negation

Negation is per component:

a := mat4 {1,;}
// Prints `mat4 {-1,0,0,0; 0,-1,0,0; 0,0,-1,0; 0,0,0,-1}`
println(-a)

Other functions for 4D vectors:

  • fn det(m: mat4) -> f64 determinant
  • fn inv(m: mat4) -> mat4 inverse
  • fn mov(v: vec4) -> mat4 translation
  • fn scale(v: vec4) -> mat4 scale
  • fn rot__axis_angle(axis: vec4, angle: f64) -> mat4 axis-angle rotation
  • n ortho__pos_right_up_forward(pos: vec4, right: vec4, up: vec4, forward: vec4) -> mat4 orthogonal view
  • fn proj__fov_near_far_ar(fov: f64, near: f64, far: f64, ar: f64) -> mat4 projection view
  • fn mvp__model_view_projection(model: mat4, view: mat4, projection: mat4) -> mat4 model-view-projection

Precision

Whenever you do calculations with 4D matrices, you get float 32 bit precision.

Arrays

An array stores a list of values. The type is [].

#![allow(unused)]
fn main() {
a := [1, 2, 3]
}

You can store values of different types in the same array:

#![allow(unused)]
fn main() {
a := [1, "hi", [1, 2, 3]]
}

To access an value inside an array, you use a number that start at 0:

#![allow(unused)]
fn main() {
a := [1, 2, 3]
println(a[0]) // prints `1`
println(a[1]) // prints `2`
println(a[2]) // prints `3`
}

Multi-dimensional arrays

An array can contain arrays. This makes it possible to represent grids in 2D, 3D etc.

#![allow(unused)]
fn main() {
a := [1, 2, 3] // 1D
println(a[0]) // prints `1`

b :=  [ // 2D
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9],
      ]
println(b[0][0]) // prints `1`

c :=  [ // 3D
          [[1, 2], [3, 4]],
          [[5, 6], [7, 8]],
      ]
println(a[0][0][0]) // prints `1`
}

Memory

Each item in an array takes 24 bytes (64 bit platforms):

  • 8 bytes for type information
  • 16 bytes for item data

This is optimized to store 4D vectors.

Objects

An object stores a list of values, organized as a dictionary. The type is {}.

#![allow(unused)]
fn main() {
a := {x: 1, y: "hi"}
}

To change a value in an object:

#![allow(unused)]
fn main() {
a.x = 2
}

Alternative:

#![allow(unused)]
fn main() {
a["x"] = 2
}

The value must be of the same type.

If you want to change the type, use :=:

#![allow(unused)]
fn main() {
a.x := "hi!"
}

Insert new keys and values with :=:

#![allow(unused)]
fn main() {
a := {}
a.x := 1
a.y := 2
a.z := 3
}

Some useful functions

  • fn has({}, str) -> bool - return true if object has key
  • fn keys({}) -> [str] - return keys of object

Links

A link in Dyon is a variable that stores bool, f64 and str efficiently. It is called "link" because it is fast and easy to put together data.

#![allow(unused)]
fn main() {
name := "John"
a := link {"Hi "name"!"}
}

The data inside a link can not be changed. You can read the data using head and tail:

#![allow(unused)]
fn main() {
a := link { 1 2 3 }
b := head(a) // `some(1)`
c := tail(a) // `link { 2 3 }`
}

When you put a link inside a link, it gets flattened:

#![allow(unused)]
fn main() {
a := link { 1 2 3 }
// `link { "start "1 2 3" end" }`
b := link { "start "a" end" }
}

Links are often used to:

  • Generate lots of data and then convert to str
  • Pre-process parts of a text template
  • Generate a web page
  • Code generation
  • Store lots of f64, bool or str in memory

Because of the easy syntax for links, you can use Dyon as a template language.

Memory

The memory of a link is divided into blocks of 1024 bytes (64 bit platforms).

A link takes only 3.2% extra memory than the ideal amount when blocks are filled.

To save memory with a link compared to an array, you need 42 items:

#![allow(unused)]
fn main() {
a := link {
        0 1 2 3 4 5 6 7 8
        9 10 11 12 13 14 15 16
        17 18 19 20 21 22 23 24
        25 26 27 28 29 30 31 32
        33 34 35 36 37 38 39 40
        41 42
    }
}

Other operators

You can use += (back) and -= (front) to link together link blocks. This is faster, but uses a little more memory.

You can not use + because it is too easy to waste memory by error.

Some useful functions

  • fn is_empty(link) -> bool - return true if link is empty
  • fn head(link) -> opt[any] - return first item in link
  • fn tail(link) -> link - return whole link except first item
  • fn tip(link) -> opt[any] - return last item in link
  • fn neck(link) -> link - return whole link except last item

Closures

A closure in Dyon is a variable that works similar to a mathematical function.

#![allow(unused)]
fn main() {
a := \(x) = x + 1
}

You can print out a closure:

#![allow(unused)]
fn main() {
// prints `\(x: any) = x + 1`
println(\(x) = x + 1)
}

To call a closure, use the \ character before the name:

#![allow(unused)]
fn main() {
a := \(x) = x + 1
println(\a(0)) // prints `1`
}

Calling closures on objects

When an object stores a closure, you can call it with named arguments:

fn main() {
    gollum := {say__msg: \(msg) = "My precious " + msg}
    // prints `my precious ring`
    println(\gollum.say(msg: "ring"))
}

Grab

Use grab to compute a value and insert it into a closure:

#![allow(unused)]
fn main() {
a := 2
b := \(x) = (grab a + 2) + x
// prints `\(x: any) = 4 + x`
println(b)
}

When a closure is inside another closure, you can use grab '2:

#![allow(unused)]
fn main() {
a := 2
b := \(x) = \(y) = (grab '2 a) + (grab x) + y
// prints `\(x: any) = \(y: any) = 2 + (grab x) + y`
println(b)
// prints `\(y: any) = 2 + 0 + y`
println(\b(0))
}

You can use grab 'N where N is the level you want to compute from.

Pro tip: Pre-compute as much as possible with grab to improve performance.

Options

In Dyon, optional values are wrapped in some(x) or none(). The type is opt, which defaults to opt[any].

The unwrap function returns the value inside some(x).

fn main() {
    a := some(5)
    if a != none() {
        println(unwrap(a)) // prints `5`
    }
}

Note to programmers accustomed to null

Many languages uses null or nil to indicate an empty reference. This leads to lots of maintenance problems, because it can appear anywhere.

Dyon uses opt whenever you would use null in another language. This forces programmers to deal with it explicitly, which reduces number of bugs in the program.

Some useful functions

  • fn some(any) -> opt[any]
  • fn none() -> opt[any]
  • fn unwrap(any) -> any

Results

In Dyon, results are wrapped in ok(x) or err(x). The type is res which defaults to res[any]. The error type is always any.

fn main() {
    a := ok(5)
    if is_ok(a) {
        println(unwrap(a)) // prints `5`
    }
}

Result is used to handle errors explicitly. Dyon has an operator ? to make this easier. You can read more about this in the chapter "Error handling".

Some useful functions

  • fn unwrap(any) -> any
  • fn unwrap_err(any) -> any
  • fn is_ok(res[any]) -> bool
  • fn is_err(res[any]) -> bool
  • fn ok(any) -> res[any]
  • fn err(any) -> res[any]

Threads

In Dyon, a thread is created with the go keyword. The type is thr, which defaults to thr[any].

fn find_sum(n: f64) -> f64 {
    return sum i n { i + 1 }
}

fn main() {
    a := go find_sum(1_000_000)
    println(unwrap(join(thread: a))) // prints `500000500000`
}

A thread runs in parallel.

Current objects are not passed between threads

Dyon does a clone of variables that are passed to a go call. The new thread starts with an empty stack. This means that current objects are not shared between threads.

There can only be one reference when joining

A thread must only have a single reference to it when joining. For example, if you store threads in an an array, you need to use pop.

Source code: examples/multi_threads

foo(i) = i + 40

fn main() {
    a := sift i 3 {go foo(i)}
    for i len(a) {
        println(unwrap(join(thread: pop(mut a))))
    }
}

Some useful functions

  • fn join__thread(thr[any]) -> res[any] - waits for the thread to finish, then returns the result

Secrets

In Dyon, a secret is a hidden array of values associated with a bool or f64. The type is sec[bool] or sec[f64].

Secrets are used in combination with any/all/max/min loops. This feature helps developers to write short and correct code.

For example, you can use secrets to find the first item in a list that satisfies a condition:

#![allow(unused)]
fn main() {
list := [1, 2, 3]
a := any i { list[i] > 2 }
if a {
    println(why(a)) // prints `[2]` because `list[2] == 3` and `3 > 2`.
}
}

It also works for lists inside lists:

#![allow(unused)]
fn main() {
list := [[1, 2], [3, 4]]
a := any i, j { list[i][j] > 2 }
if a {
    println(why(a)) // prints `[1, 0]` because `list[1][0] == 3` and `3 > 2`.
}
}

max and min loops uses the where keyword to unlock the secret:

#![allow(unused)]
fn main() {
list := [1, 2, 3, 4]
a := max i { list[i] }
println(where(a)) // prints `[3]` because `list[3]` is the greatest number.
}

Secrets are used to solve problems that otherwise would be a bit tricky to code. For example, the "minimax" algorithm which is used to pick the best move when playing a zero-sum game:

#![allow(unused)]
fn main() {
payoff := [[0, 1], [2, -1]]
a := min i {max j { payoff[i][j] }}
println(where(a)) // prints `[0, 1]` because that is the best move.
}

In the beginning secrets might feel a bit "backwards". Instead of reducing a query into a bool, one can extract information from them! Do not worry, because you will get used to it and you will like this feature when programming complex logic.

Use explain_why to add a secret to a bool:

#![allow(unused)]
fn main() {
a := explain_why(true, "hi!")
if a {
    println(why(a)) // prints `["hi!"]`
}
}

Use explain_where to add a secret to a f64:

#![allow(unused)]
fn main() {
a := explain_where(2.5, "giant")
println(where(a)) // prints `["giant"]`
}

A secret propagates from the left argument of a binary operator:

#![allow(unused)]
fn main() {
a := explain_where(2.5, "giant")
is_tall := a > 2.0
if is_tall {
    println(why(is_tall)) // prints `["giant"]`
}
}

When using a min, max, any or all, the indices are automatically added as secrets:

#![allow(unused)]
fn main() {
list := [[1, 2], [3, 4]]
println(why(any i, j { list[i][j] > 2 })) // prints `[1, 0]`
}

A secret must have meaning

The function why will only work if the value is true. This prevents programs that do not make sense, such as:

#![allow(unused)]
fn main() {
list := [1, 2, 3]
println(why(all i { list[i] < 10 }))
}
--- ERROR ---
main (source/test.dyon)
why

This does not make sense, perhaps an array is empty?
3,17:     println(why(all i { list[i] < 10 }))
3,17:                 ^

Likewise, where will only work if the value is not NaN (0/0):

#![allow(unused)]
fn main() {
list := []
println(where(min i { list[i] }))
}
--- ERROR ---
main (source/test.dyon)

This does not make sense, perhaps an array is empty?
3,19:     println(where(min i { list[i] }))
3,19:                   ^

Remember to check for empty arrays!

Overhead

Since Dyon supports 4D vectors, the size of the Variable enum is quite large. This means better performance for 4D vectors but slower performance for bool and f64. Dyon is designed for game development so this is a reasonable trade-off. The overhead from executing extra instructions for secrets is not that large because of good cache locality.

If

In Dyon, the if keyword runs a block if its condition is true:

#![allow(unused)]
fn main() {
a := 3 < 4
if a {
    println("hi!")
}
}

An else block runs when previous conditions are false:

#![allow(unused)]
fn main() {
a := 3 > 4
if a {
    println("yes!")
} else {
    println("no...")
}
}

An else if block runs when previous conditions are false and its condition is true:

#![allow(unused)]
fn main() {
a := 2
if a == 0 {
    println("=0")
} else if a == 1 {
    println("=1")
} else if a == 2 {
    println("=2")
} else {
    println(">2")
}
}

An if can be used as an expression:

#![allow(unused)]
fn main() {
a := 3
b := 3
c := 2 + if a == b { 1 } else { 0 }
}

Loops

In Dyon there are 4 kinds of loops:

  • Infinite loop
  • Traditional For loop
  • Mathematical loops
  • Link loop

Infinite loop

#![allow(unused)]
fn main() {
loop {
    println("hi!")
    sleep(1)
}
}

An infinite loop runs runs forever, or until break is used inside it.

Tip: Use Ctrl+C to terminate an infinite loop in the Terminal window.

Traditional For loop

#![allow(unused)]
fn main() {
for i := 0; i < 10; i += 1 {
    println(i)
}
}

A traditional loop is similar to the loop in the programming language C. It takes 3 expressions:

  • Initialize, called first and once, e.g. i := 0
  • Condition, checked for each turn, e.g. i < 10
  • Step, called after each turn, e.g. i += 1

Mathematical loops

Dyon is famous for its mathematical loops:

  • for - do something for each turn
  • sift - create an array out of values
  • min - find the minimum value
  • max - find the maximum value
  • /any - check whether a condition is true for any value
  • /all - check whether a condition is true for all values
  • /sum - add values to get the sum
  • /prod - multiply values to get the product
  • ∑vec4/sum_vec4 - add 4D vectors to get the sum
  • ∏vec4/prod_vec4 - multiply 4D vectors to get the product

In mathematics, it is very common to loop over an index. An index starts at 0, and increases with 1 for each turn. All mathematical loops uses the same index notation.

#![allow(unused)]
fn main() {
for i 10 {
    println(i)
}
}

This is equivalent to for i := 0; i < 10; i += 1.

You can also specify the start and end value:

#![allow(unused)]
fn main() {
for i [0, 10) {
    println(i)
}
}

The link loop is similar to a link block, but for a repeated pattern.

#![allow(unused)]
fn main() {
list := [1, 2, 3]
println(link i {(i+1)": "list[i]})
}

All evaluated expressions are appended to the link. Expressions that do not return a value are allowed inside a link loop.

Pro-tip: A link block inside a link loop gives you an all-or-nothing behavior.

fn main() {
    people := [{name: "Homer"}, {name: "Bart"}, {name: "Marge"}]
    kids := link i {link {
        "name: "
        name := people[i].name
        if name == "Bart" {continue} else {name}
        "\n"
    }}
    print(kids)
}

Break

The break keyword exits a loop:

#![allow(unused)]
fn main() {
a := 4
loop {
    a -= 1
    if a < 0 { break }
}
}

To exit an outer loop, use a label:

#![allow(unused)]
fn main() {
'outer: loop {
    loop {
        break 'outer
    }
}
}

In a mathematical loop, break skips the rest:

fn main() {
    list := [1, 2, 3]
    println(sum i {
        if i > 1 { break } // skip `3`
        list[i]
    }) // prints `3`
}

Continue

The continue keyword goes to the next turn.

#![allow(unused)]
fn main() {
loop {
    sleep(1)
    continue
    println("hi") // never called
}
}

To go to the next turn of an outer loop, use a label:

#![allow(unused)]
fn main() {
'outer: loop {
    loop {
        continue 'outer
    }
}
}

In a mathematical loop, continue skips the current item:

fn main() {
    list := [1, 2, 3, 4]
    println(sum i {
        if i == 2 { continue } // skip `3`
        list[i]
    }) // prints `7`
}

Infer range

In Dyon, the range of a mathematical loop can be inferred from the loop body.

When iterating over a list, you can leave out start and end:

#![allow(unused)]
fn main() {
list := [1, 2, 3]

for i {
    println(list[i])
}
}

This is equivalent to:

#![allow(unused)]
fn main() {
list := [1, 2, 3]

for i len(list) {
    println(list[i])
}
}

For nested loops, the indices must follow the same order as the loops:

#![allow(unused)]
fn main() {
list := [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
for i {
    for j {
        for k {
            println(list[i][j][k])
        }
    }
}
}

Packed loops

In Dyon, you can pack mathematical loops together of the same kind:

#![allow(unused)]
fn main() {
for i, j, k {
    println(list[i][j][k])
}
}

This is equivalent to:

#![allow(unused)]
fn main() {
for i {
    for j {
        for k {
            println(list[i][j][k])
        }
    }
}
}

You can also specify start and end for each index:

fn main() {
    list := [1, 2, 3, 4]
    n := len(list)

    // For each pair.
    for i n, j [i + 1, n) {
        println(abs(list[i] - list[j]))
    }
}

Pro tip: If you find a packed loop hard to understand, you can print out closure to see how it works:

fn main() {
    a := \() = {
        list := [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
        for i, j, k {
            println(list[i][j][k])
        }
    }
    println(a)
}

Current objects

In Dyon, a current object is a variable that can be accessed by name.

To create a current object, use ~ when declaring a variable:

fn main() {
    ~ list := [1, 2, 3]
    print_list()
}

// Get the current object with name `list`.
fn print_list() ~ list {
    for i { println(list[i]) }
}

A new current object overrides the old one until it runs out of scope:

fn main() {
    ~ a := 2
    foo() // prints `2`
    {
        ~ a := 3
        foo() // prints `3`
    }
    foo() // prints `2`
}

fn foo() ~ a {
    println(a)
}

Current objects are useful to store things like settings or assets.

Note to programmers accustomed to globals

Current objects replaces cases where you would use globals in another language. Dyon does not support globals, because dynamic modules should not initialize memory upon loading. A current object is like a global, except you must use it explicitly, and it has a scope which can replace other current objects. You might find it much easier to read code using current objects.

Types

In Dyon, there are two kinds of types:

  • Runtime types, which only has one level
  • Static types, which can have more than one level

Runtime types

Dyon check the runtime type when using = to change a variable. To get the runtime type, use the typeof function:

fn main() {
    println(typeof(1.2)) // prints `number`
    println(typeof(true)) // prints `boolean`
    println(typeof("hi!")) // prints `string`
    println(typeof((1, 2))) // prints `vec4`
    println(typeof([1, 2, 3])) // prints `array`
    println(typeof({a: 2})) // prints `object`
    println(typeof(link { 1 2 3 })) // prints `link`
    println(typeof(some(2))) // prints `option`
    println(typeof(ok(2))) // prints `result`
    println(typeof(go foo())) // prints `thread`
    println(typeof(\(x) = x + 1)) // prints `closure`
    println(typeof(unwrap(load("src/main.dyon")))) // prints `rust_object`
}

fn foo() -> { return ok(2) }

Static types

Function arguments can have specify a static type:

#![allow(unused)]
fn main() {
fn foo(a: f64) { ... }
}

By default this is any.

Static types are used by the type checker to check for errors:

  • any, any type
  • f64, number
  • bool, boolean
  • str, string
  • vec4, 4D vector
  • [], array, defaults to [any]
  • {}, object
  • link, link
  • opt, defaults to opt[any]
  • res, defaults to res[any]
  • thr, defaults to thr[any]
  • Closure type, e.g. \(any, ..) -> any
  • Ad-hoc type, e.g. Foo, defaults to Foo {}

There is no static type for Rust objects. Use an ad-hoc type, e.g. Foo any.

Ad-hoc types

In Dyon, an ad-hoc type is a type that you just give a name. It is not declared anywhere, but used directly when writing the functions. Dyon treats ad-hoc types as distinct from each other.

fn new_person(first_name: str, last_name: str) -> Person {
    return {
        first_name: clone(first_name),
        last_name: clone(last_name)
    }
}

fn say_hello(person: Person) {
    println("Hi " + person.first_name + "!")
}

fn main() {
    homer := new_person("Homer", "Simpson")
    say_hello(homer) // prints `Hi Homer!`
}

You can use any name that is not used by other Dyon types. It is common to use CamelCase.

Person is the same as Person {}, where {} is the inner type.

The inner type goes with the ad-hoc type and vice versa:

fn say_hello(person: Person) {
    println("Hi " + person.first_name + "!")
}

fn main() {
    say_hello({first_name: "Homer"}) // prints `Hi Homer!`
}

Addition and multiplication

Two ad-hoc types can be added if the inner type allows addition.

Dyon complains if you try to add an ad-hoc type with an inner type.

For example:

#![allow(unused)]
fn main() {
// Convert number to `km`
fn km(v: f64) -> km f64 { return v }

km(3) + km(4) // OK
km(3) + 4 // ERROR
}

Multiplication is not allowed, because this often changes physical units.

Complain when wrong

In Dyon, the type checker only complains when it knows something is wrong.

When you do not specify the type of an argument, it defaults to any:

foo(x) = x + 1

fn main() {
    println(foo(false))
}

In the program above, the type checker does not know that something is wrong.

You get a runtime error:

--- ERROR ---
main (source/test.dyon)
foo (source/test.dyon)

Invalid type for binary operator `"+"`, expected numbers, vec4s, bools or strings
1,10: foo(x) = x + 1
1,10:          ^

To get a type error, add the type to the argument:

#![allow(unused)]
fn main() {
foo(x: f64) = x + 1
}

Now the type checker complains, because it knows that false is not f64:

--- ERROR ---
In `source/test.dyon`:

Type mismatch (#100):
Expected `f64`, found `bool`
4,17:     println(foo(false))
4,17:                 ^

Static types are not guaranteed

The type of an argument is not guaranteed to be the given static type.

For example:

foo(x: f64) = x + 1

forget_type(x: any) = clone(x)

fn main() {
    println(foo(forget_type(false))) // ERROR
}

This gives you a runtime error, because the type checker is not that smart.

In-types

In Dyon, an in-type receives input data from a function.

The in keyword creates a receiver channel:

#![allow(unused)]
fn main() {
x := in log
}

Next time you call log, the input data is sent to x:

fn log(a: f64) {}

fn main() {
    x := in log
    log(1)
    println(next(x)) // Prints `some([1])`
}

Calling functions

The type is in[[]] since arguments are stored in array:

fn log(a: f64) {}

fn foo(x: in[[f64]]) {println(next(x))}

fn main() {
    x := in log
    log(1)
    foo(x)
}

Working With Threads

In-types works across threads:

fn log(a: f64) {}

fn finish() {}

fn bar(id: f64, n: f64) -> bool {
    for i n {
        log((id + 1) * 1000 + i)
        sleep(0.1)
    }
    finish()
    return true
}

fn main() {
    log := in log
    finish := in finish
    n := 10
    for i n {_ := go bar(i, 2)}
    loop {
        for msg in log {println(msg[0])}
        if n == 0 {break}
        for done in finish {n -= 1}
    }
}

Notice that n == 0 is checked after emptying the log for messages.

Other functions for in-types

  • fn next(channel: in) -> opt[any] - Blocks thread until message is received from channel.
  • fn try_next(channel: in) -> opt[any] - Checks for message on channel.

Lifetimes

In Dyon, a lifetime tells whether an argument outlives another argument.

Lifetimes are rare

In normal programming there is little need to worry about lifetimes.

For example, use clone when putting a variable inside another:

#![allow(unused)]
fn main() {
fn put(a, mut b) {
    b[0] = clone(a)
}
}

Instead of:

#![allow(unused)]
fn main() {
fn put(a: 'b, mut b) {
    b[0] = a
}
}

It is useful to know how lifetimes work, but you rarely need them in practice.

Lifetimes replaces garbage collector

Some languages can have a pointer to a variable that does not exist. When this happens, it is called "dangling pointer". This can lead to unpredictable behavior and system crashes. Languages that allow dangling pointer are unsafe, and the programmer must be extra careful.

Many languages use a garbage collector to avoid dangling pointers. Instead of removing the variable that the pointer points to, it keeps it around in memory until all its pointers are gone.

Dyon uses static lifetime checks to ensure that dangling pointers are impossible. This removes the need for a garbage collector.

Lifetimes tell the order of declaration

A lifetime tells whether an argument outlives another argument:

// `a` outlives `b`
fn put(a: 'b, mut b) {
    b[0] = a
}

fn main() {
    a := [2, 3]     // - lifetime of `a`
                    // |
    b := [[]]       // |  - lifetime of `b`
                    // |  |
    put(a, mut b)   // |  |
}

The variable "a" outlives "b" because it is declared before "b".

The same program can be written like this:

fn main() {
    a := [2, 3]
    b := [[]]
    b[0] = a
}

Here, the order of the declared variables is known.

The return lifetime

The return lifetime is the lifetime of the return variable. This outlives the default lifetime of arguments (no lifetime).

If you return one of the argument, you must use 'return or clone:

#![allow(unused)]
fn main() {
// With `'return` lifetime.
id(x: 'return) = x

// With `clone`.
id(x) = clone(x)
}

The lifetime checker does not understand types

In Dyon, the static type is not guaranteed at runtime, therefore bool, f64 and str follows same rules as [] or {}:

#![allow(unused)]
fn main() {
fn foo(a: f64) -> {
    return a // ERROR
}
}
--- ERROR ---
In `source/test.dyon`:

Requires `a: 'return`
2,12:     return a // ERROR
2,12:            ^

Lifetimes are about references

A lifetime is about the references stored inside a variable. All references outlive variables they are stored in. Variables can not store references to themselves, because it can not outlive itself.

In order to put a reference inside a variable, the lifetime checker must know that the reference outlives the variable.

Because of the lifetime checker, all memory in Dyon is an acyclic graph.

Error handling

In Dyon, error handling uses the ? operator to propagate errors.

fn try_divide(a: f64, b: f64) -> res[f64] {
    return if b == 0 { err("Division by zero") }
           else { ok(a / b) }
}

fn foo(b: f64) -> res {
    a := try_divide(5, b)?
    return ok(a + 2)
}

fn main() {
    println(foo(2)) // prints `ok(4.5)`
    println(foo(0)) // prints `err("Division by zero")`
}

When using unwrap, an error is reported with a trace of all ? operations:

#![allow(unused)]
fn main() {
println(unwrap(foo(0)))
}
--- ERROR ---
main (source/test.dyon)
unwrap

Division by zero
In function `foo` (source/test.dyon)
7,10:     a := try_divide(5, b)?
7,10:          ^

15,20:     println(unwrap(foo(0)))
15,20:                    ^

An opt is converted into res when using ?, with the error "Expected some(_), found `none()".

Copy-on-write

In Dyon, arrays and objects uses atomic reference counting (Arc).

Whenever there are two non-stack references to the same array, it will create a copy when changing the array:

fn main() {
    // `[1, 2]` is inside an array,
    // and is therefore not stored in stack memory.
    a := [[1, 2]]
    // Create another reference to `[1, 2]`
    b := [a[0]]
    b[0][0] = 3
    println(b[0][0])
    // The old `[1, 2]` array remains unchanged.
    println(a[0][0])
}

This means that methods, the building-block of OOP patterns, are impossible to use in Dyon:

fn foo(mut a) {
    a[0] = 3
}

fn main() {
    // `[1, 2]` is inside an array,
    // and is therefore not stored in stack memory.
    a := [[1, 2]]
    // When we pass it as argument,
    // the reference counter is increased.
    foo(mut a[0])
    // The value remains unchanged,
    // because `foo` changed a copy.
    println(a[0][0]) // prints `1`
}

To modify variables inside other variables, it must be declared on the stack:

fn foo(mut a) {
    a[0] = 3
}

fn main() {
    // `[1, 2]` is put on the stack.
    list := [1, 2]
    // Putting a stack reference to `list` inside `a`
    a := [list]
    // When we pass it as argument,
    // the stack reference is passed.
    foo(mut a[0])
    // The value is changed.
    println(a[0][0]) // prints `3`
}

Data oriented design

When writing programs in Dyon, you need to avoid copy-on-write. The easiest way to do this is to organize application structure in flat arrays.

  • Do not worry about having lots of arrays
  • Use current objects to pass arrays around
  • Batch changes to arrays together by iterating over them

Optimize the data structure for the algorithms that process it. When the algorithm changes, the data structure changes too!

Dyon is a scripting language

If you need OOP patterns, then there is only one solution: Use another language, for example Rust. Dyon is designed for scripting, as in solving problems fast. It is not designed for building abstractions, or proving things through the type system.

Dynamic modules

In Dyon, you organize code by using dynamic modules. It is common to write a loader script that loads modules and imports them to other modules.

Dynmod example

Source: examples/dynmod

main.dyon:

fn main() {
    math := unwrap(load("src/math.dyon"))
    game := unwrap(load(source: "src/game.dyon", imports: [math]))
    call(game, "main", [])
}

math.dyon:

#![allow(unused)]
fn main() {
add(a: f64, b: f64) = a + b
}

game.dyon:

fn main() {
    a := 2
    b := 3
    println(add(a, b)) // prints `5`
}

Calling functions in dynamic modules

Dyon peforms a runtime lifetime check of arguments for these functions:

  • fn call(module, name, args) - returns no value
  • fn call_ret(module, name, args) - returns value

Why dynamic modules?

Module loading is often an important part of a program:

  • Download updates
  • Refresh game logic for interactive programming
  • Swap between backends

For example, the same Dyon game can run on game engines that supports multiple backend APIs for rendering. In other languages you might write generic code to make it reusable across APIs. Dyon solves this by making all code reusable across APIs, as long as the dependencies implement the same set of functions.

Look up functions

In Dyon, you get all available functions with functions:

fn main() {
    println(functions())
}

You can write a program that looks up the information about a function:

fn main() {
    fs := functions()
    has_foo := any i { fs[i].name == "foo" }
    if has_foo {
        why := why(has_foo)
        println(fs[why[0]])
    }
}

fn foo() {}
{returns: "void", type: "loaded", arguments: [], name: "foo"}

Because Dyon uses dynamic modules, functions is the only standard way of obtaining this information. This can only be known in the loaded module.

Optimize performance

To optimize the performance of Dyon programs, compile with cargo build --release.

You can also get further optimization by disabling the default debug_resolve feature. This compares location of variables on the stack with the static location. It is not needed for running programs, but used to detect bugs in Dyon.

To disable debug_resolve, change the "Cargo.toml":

[dependencies.dyon]
version = "0.8.0"
features = []

Interop with Rust

Dyon is designed to work with Rust, but uses different types.

Rust uses static types only, which usually takes up less memory and runs faster. When you need to high performance, it is recommended to use Rust.

Functions example

Source: dyon/examples/functions.rs

The dyon_fn! macro lets you write a Rust function that maps to Dyon types:

#![allow(unused)]
fn main() {
#[macro_use]
extern crate dyon;

dyon_fn!{fn say_hello() {
    println!("hi!");
}}
}

Add the function to a module:

#![allow(unused)]
fn main() {
module.add(Arc::new("say_hello".into()), say_hello, Dfn {
      lts: vec![],
      tys: vec![],
      ret: Type::Void
  });
}

The dyon_obj! macro lets to map new Rust structs to Dyon objects:

#![allow(unused)]
fn main() {
pub struct Person {
    pub first_name: String,
    pub last_name: String,
    pub age: u32,
}

dyon_obj!{Person { first_name, last_name, age }}
}

All fields must implement embed::PopVariable and embed::PushVariable.

The Dfn struct

The Dfn struct describes the type information of the function signature:

  • lts - lifetimes, e.g. Lt::Default
  • tys - types, e.g. Type::F64
  • ret - return type, e.g. Type::Bool

This is used by the lifetime and type checker when loading modules.

lib.dyon

When designing Dyon libraries written in Rust, it is common to include a "lib.dyon" file in the "src" folder. This file lists all external functions as they would appear in Dyon code.

Two examples from the Dyon library:

#![allow(unused)]
fn main() {
/// Returns an array of derived information for the truth value of `var`.
/// This can be used with the value of `∃`/`any` and `∀`/`all` loops.
fn why(var: bool) -> [any] { ... }

/// Returns an array of derived information for the value of `var`.
/// This can be used with the value of `min` and `max` loops.
fn where(var: f64) -> [any] { ... }
}

Comments should start with ///.

Mutability interop

Mutability information is part of the function name:

#![allow(unused)]
fn main() {
fn foo(mut a: f64, b: f64) { ... }
}

The name of this function is foo(mut,_).

#![allow(unused)]
fn main() {
module.add(Arc::new("foo(mut,_)".into()), foo, Dfn {
      lts: vec![Lt::Default; 2],
      tys: vec![Type::F64; 2],
      ret: Type::Void
  });
}

Lifetime interop

The Lt enum describes the lifetime of an argument.

The most common lifetime is Lt::Default (no lifetime).

If you have a function like this:

#![allow(unused)]
fn main() {
fn foo(mut a: [{}], b: 'a {}) { ... }
}

Then the Dfn struct looks like this:

#![allow(unused)]
fn main() {
Dfn {
    lts: [Lt::Default, Lt::Arg(0)],
    tys: [Type::Array(Box::new(Type::Object)), Type::Object],
    ret: Type::Void
}
}

Lt::Arg(0) means the argument outlives the first argument.

Lifetimes must not be cyclic. For example, this is not valid Dyon code:

#![allow(unused)]
fn main() {
fn foo(a: 'b, b: 'a) { ... }
}

Lt::Return means the arugment outlives the return value:

#![allow(unused)]
fn main() {
fn foo(a: 'return) -> { ... }
}

Manual interface

A manual interface gives more control over the interface between Rust and Dyon. This is the only way to write external functions that mutate arguments.

The signature of an external function:

#![allow(unused)]
fn main() {
fn(rt: &mut Runtime) -> Result<(), String>
}

These functions are useful when pushing and popping variables:

  • Runtime::pop - convert from stack
  • Runtime::pop_vec4 - convert 4D vector from stack
  • Runtime::pop_mat4 - convert 4D matrix from stack
  • Runtime::var - convert from variable
  • Runtime::var_vec4 - convert 4D vector from variable
  • Runtime::push - convert to variable on stack
  • Runtime::push_vec4 - convert to 4D vector on stack
  • Runtime::push_mat4 - convert to 4D matrix on stack
  • Runtime::resolve - resolve a variable reference

Getting arguments of function

Since the Dyon runtime uses a stack, you must pop the argument in reverse order. Before you use an argument, you must use Runtime::resolve in case it is a reference to a variable:

#![allow(unused)]
fn main() {
let draw_list = rt.stack.pop().expect("There is no value on the stack");
let arr = rt.resolve(&draw_list);
}

Runtime::pop, Runtime::pop_var, Runtime::pop_vec4, Runtime::pop_mat4, Runtime::var, Runtime::var_vec4 and Runtime::var_mat4 resolves the variable for you.

Mutate argument

To mutate an argument, you need to obtain a mutable reference to the resolved variable on the stack:

#![allow(unused)]
fn main() {
let v = rt.stack.pop().expect(TINVOTS);

if let Variable::Ref(ind) = v {
    let ok = if let Variable::Array(ref mut arr) = rt.stack[ind] {
        Arc::make_mut(arr)...;
    }
}
}

Return value

After popping argument and computing a value, push the result on the stack. Do not push more than one value, since Dyon only supports a single return value.

Reading from a Dyon variable

The Runtime::var function converts a value inside a variable:

#![allow(unused)]
fn main() {
let radius: f64 = rt.var(&it[2])?;
}

4D vectors

The Runtime::var_vec4 function converts to a vec4 convertible type:

#![allow(unused)]
fn main() {
let color: [f32; 4] = rt.var_vec4(&it[1])?;
}

4D matrices

The Runtime::var_mat4 function converts to a mat4 convertiable type:

#![allow(unused)]
fn main() {
let mat: [[f32; 4]; 4] = rt.var_mat4(&it[1])?;
}

Piston-Current

Dyon keeps no track of variables in the Rust environment. You can use the Piston-Current crate to read from or change such variables by type.

#![allow(unused)]
fn main() {
pub fn render(rt: &mut Runtime) -> Result<(), String> {
    rt.push(unsafe { Current::<Option<Event>>::new()
        .as_ref().expect(NO_EVENT).render_args().is_some() });
    Ok(())
}
}

Standard Library

Dyon organizes functions in 3 ways:

  1. Dyon modules (also called "loaded functions")
  2. Rust interop (also called "external functions")
  3. Standard Library (also called "intrinsics")
  1. and 2) are explained in previous chapters.

This chapter is about the third point 3) Standard Library.

This is what the Standard Library contains:

  • Functions for loading modules and calling functions
  • Functions for using Dyon's built-in types
  • Functions for loading and saving data
  • Functions for meta-parsing

The standard library of Dyon is small compared to e.g. Rust (less than 150 functions). If you want to know the details of how it works, you can read the source code here.

Some functions are in the standard library because they use special knowledge that is not available in external functions.

This chapter shows some things you can do with the Standard Library.

Command-line arguments

Dyon has a function to return an array of command-line arguments:

#![allow(unused)]
fn main() {
fn print_args_list {
    println(args_os())
}
}

The first item is the path of the executable.