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!