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.