Scripting without garbage collector

Dyon is a rusty dynamically typed programming language, previously called “Dynamo”.

This article is to explain a bit more in depth about the language. In particular, we will see how the lack of a garbage collector impacts how you think when programming.

Lifetimes

Dyon uses a lifetime checker instead of a garbage collector.

For example, consider the following program:

fn foo(a, b) {
    a.x = b
}

fn main() {
    a := {x: [0]}
    b := [5]
    foo(a, b)
}

Dyon won’t let you run it, because storing b inside a requires it to outlive a:

Function `foo` requires `b: 'a`
2,11:     a.x = b
2,11:           ^

One way to fix it is to write clone(b), but you can also do as Dyon says:

fn foo(a, b: 'a) {
    a.x = b
}

Now there is another error:

`b` does not live long enough
4,12:     foo(a, b)
4,12:            ^

This is because b is declared after a:

fn main() {
    a := {x: [0]}
    b := [5] // `b` does not outlive `a`
    foo(a, b)
}

By moving it before a, the program works:

fn main() {
    b := [5] // `b` outlives `a`
    a := {x: [0]}
    foo(a, b)
}

The Rust borrow checker behaves similarly.
If you like a challenge, try fix this code: http://is.gd/IvQdqS
Solution: http://is.gd/SX3z1J

Data oriented design

Dyon shares similar semantics to Rust, but there are some differences:

  • Can not reference memory outside the stack
  • Variables are mutable
  • An object can be shared among other objects

If you thought Rust was picky on how you designed your data structures, then Dyon is even more so!

How the code looks can have a great impact on performance. To optimize, you have to carefully declare variables and reference them.

For example, whenever you declare a variable and assign it a reference, you make a copy:

a := [1, 2, 3]
b := a // creates a copy of the list `a`

If you have a long list of 10 000 items, this can be very expensive.

More examples:

b := {list: a} // cheap, since `a` is referenced
c := a.list // expensive, since `a.list` is copied
d := a.list[0] // cheap, since only `1` is copied

This has the consequence that Dyon does not like deep trees by design.
You are forced to process things in flat arrays as much as possible.

With other words, Dyon forces programmers to use data oriented design.

Named arguments syntax

Since Dyon likes flat arrays, calls to functions tends to have lots of arguments:

update(physics_settings, dynamic_objects, static_objects)

The problem is to remember the order.

To solve this, named argument syntax is supported through snake case function names:

update_physicsSettings_dynamicObjects_staticObjects(p, d, s) {
    ...
}

fn main() {
    ...
    update(physicsSettings: physics,
           dynamicObjects:  dynamic_objects,
           staticObjects:   static_objects)
    ...
}

This design has the benefits:

  • Code does not break when argument name changes
  • A way to resolve conflicts between function names
  • Helps understanding the code

Dynamic modules

The way you organize code in Dyon is very different from Rust:

  • There is no mod keyword to declare a module
  • There is no use keyword to import a module

One file of Dyon script does not know that another exists!

Instead, Dyon uses dynamic modules:

loader.dyon:

fn main() {
    graphics := load("graphics.dyon")
    window := load("window.dyon")
    my_program := load(source: "my_program.dyon", imports: [graphics, window])
    call(my_program, "main", [])
}

The imported modules become part of the prelude in the loaded module.

my_program.dyon:

fn main() {
    // No need to import anything.
    window := create_window(title: "hello world!", size: [512; 2])
    ...
}

This design has the benefits:

  • Controlling the prelude is convenient when prototyping
  • By writing a “loader” script, you do not need to recompile
  • Gamedev often results in lots of smaller programs experimenting with different ideas
  • Flexible control of where the script is stored
  • Easy to reuse code even you did not plan to
  • Swapping out backends is as easy to changing a module with another
  • Refresh modules to make instant changes while running

My thoughts so far

I think of Dyon as a domain specific language for gamedev:

  • It is very convenient for testing ideas rapidly
  • It does not need a garbage collector
  • It forces you to think in a data oriented way

It is a language for prototyping, and can not match Rust on performance. However, it is fast enough to be used in simple games. When an idea grows in a large program, you can refactor it out into modules. Later, you can rewrite parts of the code in Rust to make it faster. This combination of flexibility and power is exactly what I am looking for.

I hope you will enjoy Dyon as much as I have!

Dynamo

One week ago, I sat down and wrote some ideas about a dynamically typed language:

  • Simple but convenient
  • Garbage collected (later dropped in favor of lifetime checking)
  • Easy to integrate with Rust

The more I thought about it, the more I wanted to make it, so I just stopped writing ideas and started coding…

… and it was so fun that I could not stop.

Say hello to Dynamo!

It is not even newborn yet, but you are welcome to join the project!

About the development

For parsing the grammar I used Piston-Meta, so it was possible to fix the syntax easily while working.

  • // for single line comments
  • /* */ for multi line comments that can be nested
  • operators are built into the grammar

:= for declaring, = for mutable assignment

One thing that annoyed me from working in earlier scripting languages is that you can accidentally assign a string where you want a number. So the idea hit me that = could check the type:

fn main() {
    a := 2
    a = "hi" // ERROR: Expected assigning to text
}

Planning to use := for inserting a new property in an object, while = requires it to exist:

fn main() {
    a := {x: 0, y: 0}
    a.z = 2 // ERROR: Object has no key `z`
}

C/Go-like for loops

fn main() {
    for i := j; i < n; i += 1 {
        println(i)
    }
}

Rusty if-expressions

fn main() {
    b := if true { 0 } else { 1 }
}

You can also do this:

fn main() {
    b := {
        x := 0
        x + 5
    }
}

Return

Functions that returns something uses -> in front of the brackets, like Rust.

An idea is to allow return as a variable:

fn foo() -> {
    return = 2 // set returned value without exiting function
    return 3 // set returned value and exit function
}

Rusty labels on for and loop

fn main() {
    'outer: loop {
        loop {
            break 'outer
        }
    }
}

Javascript-like objects and arrays

So far I had not use for null. Can use {} or [] instead. Since [] is Vec in Rust under the hood, it does not allocate.

fn main() {
    pos := {x: 0, y: 0}
    mat := [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
    ]
    obj := {}
}

Lifetime checking

When figuring out how to make it work without a garbage collector, I decided to write a statically analyzed lifetime checker, like Rust does.

fn foo(a) {
    pos := {x: 0, y: 0}
    a.pos = pos // ERROR: `pos` does not live long enough
}

You need to clone objects that do not live long enough:

fn foo(a) {
    pos := {x: 0, y: 0}
    a.pos = clone(pos) // OK
}

Dynamo also understands this for arguments:

fn foo(a, pos) {
    a.pos = pos // ERROR: Requires `pos: 'a`
}

Fix it, and it runs:

fn foo(a, pos: 'a) {
    a.pos = pos // OK
}

It also understands returns:

fn x(x) -> {
    return {x: x, y: 0} // ERROR: Requires `x: 'return`
}

Again, just do as Dynamo says:

fn x(x: 'return) -> { // This means `x` outlives the returned value
    return {x: x, y: 0} // OK
}

When you call a such function, you need to reference the argument:

fn x(x: 'return) -> {
    return {x: x, y: 0}
}

fn main() {
    println(x([0, 1])) // ERROR: Requires reference to variable
}

This happens because the argument need to outlive the returned value, so the way you do that is declaring a variable before passing it to the function:

fn x(x: 'return) -> {
    return {x: x, y: 0}
}

fn main() {
    foo := [0, 1]
    println(x(foo)) // OK
}

How fast is it to parse and run?

In a release build, a simple program takes a few milliseconds:

Empty program:

fn main() {}
$ cargo bench main
     Running target/release/dynamo-599221bc629f58af

running 1 test
test tests::bench_main    ... bench:   2,018,252 ns/iter (+/- 45,396)

Incrementing a number 100 000 times:

fn main() {
    x := 0
    for i := 0; i < 100_000; i += 1 {
        x += 1
    }
}
$ cargo bench add
     Running target/release/dynamo-599221bc629f58af

running 1 test
test tests::bench_add_two ... bench:  23,731,639 ns/iter (+/- 627,911)

This is what happens:

  1. Piston-Meta parses the source with the meta syntax
  2. The main thread constructs the AST, while another thread does lifetime checking directly on meta data
  3. If the program passes the lifetime check, it creates a runtime environment
  4. Walks over the AST and executes the program

Future work

  • Booleans
  • Better error reporting
  • Better way of handling intrinsic functions?
  • Modules?
  • First class functions?

Want to join working on Dynamo?

Not sure if it is going to be useful yet, but if it does I would like to use it write some small games. Except from that, the goals and suggestions are completely up to the people working on the project.

If you have question or want to work on something, open up an issue here.

What is happening 2

Here is a new update on what is happening in the Piston project!

Image

This is a very popular library to read and write image formats. All code is Rust, except the flate2 dependency. It was started by ccgn and continued by nwin.

nwin cleaned up the API in the codec libraries. Some bugs were fixed, thanks to bfops, lgvz and froydnj.

Conrod

mitchmindtree landed new design. This was an epic PR.

You can now update the UI outside the render event. This means you can change the updates-per-frame to a lower rate for reduced CPU usage:

// Poll events from the window.
for event in window.ups(60) {
    ui.handle_event(&event);
    event.update(|_| ui.set_widgets(set_widgets));
    event.draw_2d(|c, g| ui.draw_if_changed(c, g));
}

The draw_if_changed method renders the UI to the frame buffers. By default this is 3 times since triple buffers are common, but you can change this with a setting. If the UI is not animated, this saves energy usage significantly.

The benchmarks we have done so far indicates that the performance is approximately the same as previous design. This could indicate that more use of hash functions is a tradeoff with widgets as primitives. It could also mean that we are running into the limit of immediate design of piston-graphics.

We also discussed making Conrod backend agnostic on a higher level of graphics API, but have not come up with a good design yet. One important thing is to not put severe restrictions on the type of widgets that can be made.

Piston-Graphics

Piston-Graphics is a backend agnostic library for 2D graphics, using immediate design. It was one of the first libraries in the Piston project and have been redesigned multiple times over the past 2 years.

Some default trait method were added to make it possible to do further optimization in the backend. The docs were improved to make it easier to learn how to write a new backend.

One benefit of immedate design is the flexibility and easy to make new backends. A downside is performance since it needs to move data to the GPU.

tomaka has suggested that a graphics API could be built on top of texture arrays and persistent mapped buffers. This technique has the advantage of reducing draw calls, boosting performance on systems with high driver overhead. Think of it as closer to O(1) compared to O(N) when rendering is limited by CPU usage. Here is an article if you are interested.

If somebody wants to start a new library to test a such design, open an issue here. This could be done over Glium, raw OpenGL or possibly Gfx.

Imageproc

This project started by theotherphil to separate image processing from image formats in the image library.

More people are now getting interested, and there has been some discussion about the overlap with computer vision. Since this is a large field, should we start a new organization, like we did with RustAudio?

If you are new to Rust but interested in image processing and computer vision, this is a project for you!

Turbine

This is a project with plans to make a 3D game engine with built-in editor. It uses simple Entity-Component-System and one file for every entity for easier version control. The goal is to connect together some of the tech we already have developed.

mitchmindtree and I tested and fixed some bugs when displaying Conrod UI on top of 3D.

Hematite

The project goal is to develop a Minecraft clone for client and server.

One the client side:

  • limeburst donated the “hematite” name on crates.io :)
  • Some bugs were fixed, and toqueteos landed world auto-finding, an epic PR that was broken for months.

On the server side:

TrueType

This is an attempt to get a pure Rust alternative to read and rasterize TrueType fonts.

I got it compiling by porting it from C, but expected it to never actually work. To my delightful surprise PeterReid made it! zummenix continued cleaning it up and improving it.

Mix-Economy

This is a new library to regulate virtual economies based on an experimental idea.

I got this idea from Alan Watts actually, that money could be thought of as a pure abstraction. After all, the goal of money is to make people collaborate toward complex goals which the monetary system is blind to. This means when trying to achieve new goals, for example sustainable development under near full automation, the monetary system might need to be redesigned. We live in a new time where the old ideas of money are challenged, which is exciting.

One big problem with conventional economics is the difficulty with reducing economic inequality. Putting the individual interests aside, severe economic inequality reduces economic growth. It can be thought of as less ability to reach the complex goals the system is supposed to do.

This is not just a problem with real economies, but also in virtual economies, such as MMOs. Actually, virtual economies seems to have worse Gini coefficient than any market in the real world! Some companies have people working full time to balance economies so the gameplay attracts both hardcore and causual players.

I am not an economist, so I think the experts should deal with real world problems. However, for virtual economies which are just for fun, there is no harm in testing out new ideas.

Every economy needs an outlet, which is either the poor, the rich, or a mix of both. Some players have less time to play than others, in the same way some people have less resources to contribute to an economy in the real world. A game is often about something more than maximizing money, so there is a complex, unknown goal that the algorithm is blind to.

One way to reduce inequality, is to offer everybody a basic income or a negative income tax. However, in an MMO world where the algorithms have perfect information, I figured out it would be easier to use a negative fortune tax charged by lack of money under a normalized limit. This means, rich players are rewarded more than poor players, up to a soft limit where it starts to “burn in the pocket”.

Normalizing the algorithm makes it easier to mix with an existing economy, to find a sweet spot that is fun to play. An idea is to test this for Project R, possibly built on top of Turbine. However, that’s long down the road and very uncertain.

Pluto

The goal is to make a game competiton website using pure Rust server stack.

I experimented with a droplet on pluto.rs. Seems broken at the moment :P

Detecting O(N) performance behavior

Working on Turbine and testing the performance of Conrod, I thought of a way to detecting O(N) behavior without internal profiling.

For now, it is just a spreadsheet.

The method is simple: You run the program in benchmark mode with different settings, and the formulas filter out initial overhead, initial overhead per item, and predicts linear accuracy.

It might be useful for cases when you do not have time to test for lots of different settings.

Happy new year!

Older Newer