Dyon 0.8 is released!

Dyon is a scripting language that I am working on, and has now reached version 0.8.

Yeeehaaaaaa!

In the last blog post I wrote a summary of v0.7 and about the upcoming features in v0.8.

Also, I promised this release to be awesome.

But first, some important information for new people based on the feedback from previous post:

Why Dyon is NOT like other languages

Dyon does not have a garbage collector, and does not have ownership semantics.
It uses a lifetime checker, simpler, but less powerful than Rust.

foo(a: 'b, b) { // `a` outlives `b`
    ...
}

This means that Dyon has an extremely limited memory model compared to most languages.
The whole language semantics and syntax is designed to work around this limitation:

  • Atomic reference counting of arrays and objects
  • Copy-on-write for mutable aliased memory
  • Mathematical loops using index notation

The number one issue for some people responding to last blog post, was the lack of iterators.
Using iterators in Dyon is problematic because it leads to copy-on-write.

There is also another reason why iterators are not the focus in this language:

Mathematical loops works better with data-oriented patterns!

A consequence of the memory model is that object-oriented patterns does not work at all. For example, if you try to mutate an object by calling a method:

fn main() {
    list := [{name: "John"}]
    change_name(mut list[0], "Peter")
    println(list[0].name) // prints `John`.
}

fn change_name(mut person: {}, name: str) {
    person.name = clone(name)
}

This prints out “John” instead “Peter”. The reference counter of the item in the list increases, which leads to copy-on-write when changing it.

Therefore, you can not use object-oriented programming AT ALL in Dyon.
Instead, Dyon uses data-oriented patterns, which basically means:

Put stuff in flat arrays and iterate over them.

To make this convenient, Dyon uses current objects and mathematical loops:

fn main() {
    ~ list := [{name: "John"}] // Make `list` a current object.
    foo()
    println(list[0].name) // Prints `Peter`.
}

fn foo() {
    update_names()
}

fn update_names() ~ mut list: [] { // Bring back `list` into lexical scope
    for i { // Infer range from loop body.
        list[i].name = "Peter"
    }
}

When you are iterating over an array of arrays of arrays (3D), you can write:

for i, j, k {
    list[i][j][k] = ...
}

Instead of iterators, Dyon has many loops for common iterator functionality:

  • any, e.g. a := any i { list[i] > 2 }
  • all, e.g. a := all i { list[i] > 2 }
  • min, e.g. a := min i { list[i] }
  • max, e.g. a := max i { list[i] }
  • sift, creates a new list e.g. a := sift i { list[i] * 2 }
  • sum, e.g. a := sum i { list[i] }
  • prod, e.g. a := prod i { list[i] }

You can use continue and break with these loops to do filtering.

Once you have such loops, it makes sense to add secrets:

fn main() {
    list := [1, 2, 3]
    a := any i { list[i] == 2 }
    if a {
        println(why(a)) // prints `[1]`, because it is the indices of why `list[i] == 2`
    }
}

A secret is meta data for bool and f64 that returns the indices of the loops making up the answer.
It works with any combination of any, all, min and max loops.

Some people, who might be more familiar with mathematics, love this.

To the others who do not like it:

Using Dyon helped me understand mathematics better, so perhaps it could help you too?

Anyway, that is how the language works at the moment, so now you know there is for a good reason.

My experiences with Dyon so far

This was not supposed to be serious blog post, but for celebrating the release!
I wanted this to be a special moment, because I have worked so long and finally thinking “I have made it!”.
Therefore, I will share my thoughts with you about Dyon in a way that is more like a party speech:

When I released v0.7, I thought about Dyon as scripting language that was better in some ways,
than my experiences with other scripting languages.
Incomplete, full of bugs, but promising.

Now, I think it is simply the best scripting language I have ever used.

Do not misunderstand me, when I say “best scripting language” I mean just that.
I do not mean “best programming language”.
There are lots of stuff Dyon can not do, which Rust is a better language for.

However, when it comes to scripting, I mean, “scripting” as in scripting,
or how should I put it …

… if you imagine a landscape with elevation proportional to “scripting experience”, then I think Dyon is Mount Everest.

It is far from perfect, but Mount Everest is far from perfect either, it just …

… is a total mountain feeling to that scripting experience, if you understand what I mean?

Tomorrow some people will try it out and not like it,
because they have not grown into Dyon like I did,
but I do not think it matters.

I do not know when it will reach 1.0, but it feels like if there is a moment where Dyon went from being “almost something” to “something”, then I think it is this release.

Happy birthday Dyon! Let us celebrate!

One more thing …

Since the last blog post, I added some new features.

Dyon got closures:

fn main() {
    a := \(x) = x + 1
    println("0." + str(\a(7))) // prints `0.8`
}

You can use grab to capture a value from the closure environment:

fn main() {
    name := "Dyon"
    a := \(x) = (grab name) + " 0." + str(x)
    println(\a(8)) // prints `Dyon 0.8`
}

A grab expression is read-only, because it is a partial evaluation operator.
It computes a value, and injects it into the AST.
This means you can improve performance by doing expensive computations when creating the closure.

You can print out closures to see what they do:

fn main() {
    name := "Dyon"
    a := \(x) = (grab name) + " 0." + str(x)
    println(a) // prints `\(x: any) = "Dyon" + " 0." + str(x)`
}

Oh, we can improve the performance of this closure!

fn main() {
    name := "Dyon"
    a := \(x) = (grab name + " 0.") + str(x) // Put strings together when creating the closure.
    println(a) // prints `\(x: any) = "Dyon 0." + str(x)`
}

To grab values from higher closer environments, use grab 'n <expr>. For example, here is a function that takes two functors and joins them together to one functor:

fn fjoin(
    a: \(\(any) -> any, any) -> any,
    b: \(\(any) -> any, any) -> any
) -> \(\(any) -> any, any) -> any {
    return \(f, v) = {
        a := grab a
        \a(\(v) = {
            b := grab '2 b
            \b(grab f, v)
        }, v)
    }
}

Check out the functor example to see more.

Closures must use current objects to mutate the environment.

The benefit of this design:

  • closures do not require a lifetime
  • can be returned from functions
  • easy to refactor to a function or vice versa

Beware upcoming Dyon v0.8 - It will be awesome!

Dyon, a scripting language for Rust game engines, is getting close to v0.8. For the v0.8 release, I want to polish the type checker a bit before publishing on crates.io, which might take some time. This is why I write this article about the features beforehand, so you know what to expect from the next release.

A summary of Dyon v0.7

Dyon does not use a garbage collector, but a lifetime checker. There is no borrow checker like in Rust. You annotate arguments with the lifetime of another argument that it outlives:

fn foo(a: 'b, b) { ... } // `a` outlives `b`

There is a 'return lifetime to tell that an argument outlives the return value:

fn foo(a: 'return) -> { ... }

You can assign to return like it was a normal local variable, then let the function exit the scope.

fn foo() -> { return = 3 }

Dyon has a similar object model to Javascript. You declare variables with :=:

a := [1, 2, 3] // array
b := {x: 0, y: 0} // object
c := true // bool
d := 5.3 // float with 64 bit precision

A variable with same name shadows the previous one:

a := 0
a := false
println(a) // prints `false`

To mutate a variable, you use = which performs a check that the type is the same:

a := 0
a = false // ERROR: Expected assigning to a `bool`

Objects can override the type of a property with :=, which also inserts a key if it does not exists:

a := {}
a.x := 0 // Inserts `x: 0`

There is no null, but you can use opt or res, which are similar to Option and Result in Rust:

a := some(5.3)
b := ok(5.3)

You can propagate errors with ?, which is similar to the try! macro in Rust:

fn foo() -> {
    x := bar()? // return error if something wrong happened in `bar`
    return ok(x > 3)
}

_ := unwrap(foo()) // Shows a trace if something went wrong.

Example of an error message:

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

Something went wrong!
In function `foo` (source/test.dyon)
6,10:     x := bar()?
6,10:          ^

2,17:     _ := unwrap(foo())
2,17:                 ^

Dyon supports declaring functions the same way as in mathematics:

f(x) = x/(x-1)
pi() = 3.14

When mutating an argument, you put mut in front of it:

fn foo(mut a) { ... }

When calling a function that mutates an argument, you also put mut in front of the argument:

a := [1, 2, 3]
foo(mut a)

Named argument syntax based on snake case:

foo(bar: x) // named argument syntax
foo_bar(x)

If expression:

a := if b < c { 0 } else { 1 }

Traditional For loop:

for i := 0; i < 10; i += 1 { ... }

Short For loop:

for i 10 { ... }
for i [2, 10) { ... } // with offset

Mathematical loops, with unicode symbol alternatives:

x := sum i { list[i] }
x :=  i { list[i] }
y := any i { list[i] == 0 }
y :=  i { list[i] == 0 }
z := all i { list[i] > 0 }
z :=  i { list[i] > 0 }
min_val := min i { list[i] }
max_val := max i { list[i] }

Infinite loop, like in Rust:

loop { ... }
'name: loop { break 'name } // break out of loop `name`

Dynamic modules allows a flexible way of organizing code:

m := unwrap(load("script.dyon"))
call(m, "main", [])

Import modules to the prelude when loading a new module:

m := unwrap(load(source: "script.dyon", imports: [window, graphics]))

Optional type system, that complains when proven wrong:

fn could(list: []) -> f64

Go-like coroutines for multi-threading, using the go keyword:

t := go do_something(a, b)
res := unwrap(join(thread: t))

4D vectors:

a := (x, y) // same as `(x, y, 0, 0)`
b := #ff00aa // HTML hex color encoded as 4D vector

Upcoming features in v0.8

dobkeratops had a brilliant idea: What if For loops inferred the range from the body, just like in mathematical notation?

// No need to type `len(list)`, because Dyon figures it out by looking at the code.
for i {
    foo(list[i])
}

This gave me another idea, which was to pack loops of same kind together:

// Set random weights in neural network.
for i, j, k {
    tensor[i][j][k] = random()
}

Then to make /any and /all loops work nicely min/max, I added a feature called “secrets” that gives you the indices from any composition of these loops:

x :=  i { max j { list[i][j] } < 0.5 }
if x {
    pos := why(x) // `[i, j]`
    ...
}

A secret propagates from the left argument of binary operators.

There is a why(bool) -> [any] and where(f64) -> [any].

You can add information to a secret with explain_why(bool, any) -> bool and explain_where(f64, any) -> bool:

x := any i { explain_why(did(person: person[i], said: "Doh!"), "Are you Homer Simpson?") }
if x {
    pos := why(x) // `[i, question]`
    ask(person: person[pos[0]], question: pos[1]) // Asks "Are you Homer Simpson?"
}

Dyon does not have globals, but uses a ~ to mark variables as “current object” for its scope:

fn main() {
    ~ settings := init_settings()
    foo()
}

// `foo` calls `bar` without knowing about `settings`
fn foo() {
    bar()
}

fn bar() ~ settings {
    // Can use `settings` as if it was a function argument.
}

To make code scale with size of project, but work nicely with dynamic modules, I added ad-hoc types:

// `Character` is not declared, but has an inner type `{}`.
fn new_character(name: str) -> Character {} {
    return {name: name}
}

fn greet_character(character: Character {}) {
    println("Hi " + character.name + "!")
    println("How are you doing?")
}

// Can pass values of the inner type.
greet_character({name: "Homer"})

This works for checking physical units:

fn main() {
    println(km(3) + m(5))
}

fn km(val: f64) -> km f64 { val }
fn m(val: f64) -> m f64 { val }

Addition of same ad-hoc types are allowed, but multiplication is not allowed for same ad-hoc types, since this often changes the physical unit.

Type mismatch: Binary operator can not be used with `km f64` and `m f64`
2,21:     println(km(3) + m(5))
2,21:                     ^

Dyon has a link type, which stores bool, f64 and str efficiently. It also puts them together faster than using arrays.

a := link { "hi" 5 " "true" man show" }
println(a) // prints `hi5 true man show`

You can use head and tail to process a link:

fn main() {
    a := link {"hi" 5 " "true" man show"}
    loop {
        head := head(a)
        if head == none() { break }
        println(typeof(unwrap(head)))
        a = tail(a)
    }
    /*
    string
    number
    string
    boolean
    string
    */
}

A link inside a link gets flattened, which is useful when generating text documents.

For example, you could have a smart html sanitizing function that understood tags, and then you could generate a web page any way you liked, as long as you use separate tokens for tags. Perhaps a nice idea for a web framework?

fn main() {
    title := "Welcome to my website"
    data := get_data()
    loop {
        wait_for_request()
        respond(web_page(html(link {
            "<html>"
             "<body>"
              "<h1>"title"</h1>"
              menu(data)
              overview(data)
             "</body>"
            "</html>"
        })))
    }
}

// Sanitize html.
fn html(input: link) -> link { ... }
fn menu(data: Data{}) -> link { ... }
fn overview(data: Data {}) -> link { ... }

4D vectors now has a “unpacking” feature:

fn main() {
    v := (1, 2)
    // Call `foo` with 2 arguments.
    foo(xy v)
}

fn foo(x: f64, y: f64) { ... }

This works well with snake_case named argument syntax:

fn main() {
    v := (1, 2)
    foo(x_y: xy v)
}

fn foo_x_y(x: f64, y: f64) {}

You can also swizzle vector components:

v := (1, 2, 3)
u := (zy v,) // (3, 2)

And you can use vec2/vec3/vec4 un-loops to unroll the loop and set rest of components to 0:

v := vec3 i i+1 // `(1, 2, 3, 0)`

There is a s(v: vec4, ind: f64) -> f64 function that returns a component of a 4D vector.

I also added some macros to make embedding with Rust easier:

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

You still need to register functions, because Dyon needs type + lifetime information. I recommend checking out the functions example for more information.

That is all for now!

Dyon 0.7 - Editor plugin for Atom

v0.7 is released! See PR for new features.

Also, started dyon_interactive, a library for interactive programming with the Piston game engine.

What is Dyon?

Dyon is a rusty dynamically typed scripting language

… without a garbage collector, but uses a lifetime checker.

… and has a simple optional type system and a mutability checker.

… and has Go-like coroutines, Javascript-like object model, Rusty lifetimes.

… and got space-insensitive syntax, but no semicolons (except in the ancient For loop).

… and 4D vectors, built-in syntax for HTML colors, and dynamic modules.

… and you can use return as variable, mathematical declaration of functions, and mathematical loops like ∑/sum, min, max, sift, ∃/any, ∀/all loops.

… and got some nice syntax for vector algebra.

… and meta parsing, a result of Piston research (more information about this later).

… you know, for game scripting and such!

See List of features for more information.

I know what Dyon is, where is the editor plugin?!

Install instructions

screenshot

Sorry

I can’t write a long post this time.

Got to go, there is nice weather outside :)

Older Newer