Giter Site home page Giter Site logo

dyon's Introduction

Dyon

A rusty dynamically typed scripting language

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

Tutorial
Dyon-Interactive
Dyon Snippets
/r/dyon

Dyon script files end with .dyon.

To install Dyon REPL, type:

cargo install --example dyon dyon

Then, to run the Dyon REPL, type:

dyon

To run Dyon script files from command line, type:

cargo install --example dyonrun dyon

Then, to run a script file you type:

dyonrun <file.dyon>

Editor-plugins

Dyon for Atom
Dyon for Vim
Dyon for Visual Studio Code

coding

List of features

Why the name Dyon?

Dyon is a hypothetical particle predicted by several grand unified theories in physics with both electrical and magnetic charge. See this Wikipedia article for more information.

The name Dyon fits because, just like the particle, there are things that are yet to be discovered about language design. However, this language was not born out of a grand new vision, but is the result of exploring and testing new ideas.

Motivation and goals

Sven Nilsen started this project in early 2016. The idea was to make a simple, but convenient scripting language that integrated well with Rust.

  • During the first week of coding, a way to do lifetime checking on function arguments was discovered
  • A different approach to code organization was explored by adding the ability to dynamically load modules
  • For nice error handling, added option, result and ? operator
  • To test the design of the language, created a demo for interactive coding
  • Mutability check to improve readability
  • Short For loop to improve readability and performance
  • Mathematical loops and Unicode symbols to improve readability
  • Go-like coroutines to add multi-thread support
  • 4D vectors with unpack and swizzle to make 2D and 3D programming easier
  • Html hex colors to make copying colors from image editors possible
  • Optional type system to help scaling a project
  • Ad-hoc types for extra type safety
  • Current objects to improve prototyping and tailored environments
  • Macros for easier embedding with Rust
  • Secrets to automatically derive meaning from mathematical loops
  • Closures that can be printed out, use current objects and grab from closure environment
  • Type safety for secrets, easy load/save of Dyon data
  • Link loop for easier and faster code generation and templates
  • In-types for easy cross thread communication
  • Lazy invariants, simple refinement types and binary operator overloading

Main goals:

  • Integrate well with Rust
  • Flexible way of organizing code

Performance will be optimized for the cycle:

coding -> parsing -> running -> debugging -> coding

Sub goals:

  • Safety

Non-goals:

  • Rust equivalent performance
  • Replace Rust to build libraries
  • Interfacing with other languages than Rust

License

Licensed under either of

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be dual licensed as above, without any additional terms or conditions.

dyon's People

Contributors

bvssvni avatar cedric-h avatar codehz avatar disjukr avatar lucianbuzzo avatar lucklove avatar martinlindhe avatar mikeando avatar salmmanfred avatar theneikos avatar u5surf avatar valpackett avatar wackbyte avatar yanchith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dyon's Issues

Define some broad goals/use cases

Given it's place in the Piston project, I've been assuming that dynamo is intended to be used for game scripting. However, that's not explicitly mentioned anywhere and I don't want to spend time writing a proposal that turns out to be inappropriate for your vision of the language.

If it is game scripting, what kind of level are you expecting it to be best at (keeping in mind that people will contort any language into any situation with enough effort)? Is it intended to be mainly used for large amounts of game logic, or for small bits of custom behaviour?

I'd like to help with the language, but I don't really know anything about the language to do so.

Redesign AST to use Vec<Expression>

ast::Expression is the most central node in the AST. Currently it uses a lot of Box. It might be that reserving some memory upfront and packing the nodes in a Vec will improve performance a bit.

  • Redesign to use usize and Vec<Expression>
  • Test performance before merging

Idea for "modules": External loading into prelude

In Rust you write like this:

mod foo; // looks for file "foo.rs" or "foo/mod.rs"

fn main() {
    foo::hello_world(); // call function within module
}

I have an idea for a setup that might fit better for scripting. Modules are loaded using the same scripting language, then the functions becomes part of the prelude for a script that is loaded and checked at runtime.

Example

foo.rs:

fn hello_world() {
    println("hello world!")
}

loader.rs

fn main() {
    foo := load(module: "foo.rs")
    my_program := load(source: "my_program.rs", modules: [foo])
    call(module: my_program, function: "main", arguments: [])
}

my_program.rs:

fn main() {
    hello_world() // hello_world is part of prelude
}

Motivation

The technique is to use "bootstrapping" as a way of organizing code.

  • The language remains simple
  • Different backends can be implemented as modules
  • The loader can be written as an installer script
  • Loader logic can be written in Rust, so a loader script can be used for prototyping
  • Modules are first class citizens

Fake type checking - use argument name substitution

Instead of providing full type checking, one could do a different kind of type checking that is closer to faking it entirely, using argument name substitution. This means that instead of types, one associates an argument name with a variable which is used to append _<type> when calling a function, if there is no specified argument name. If there are no argument names and the function is not found, then it uses the short name.

Example:

c := [1; 4]
r := [0, 0, 100, 100]
draw(color: c, rectangle: r)

In Dyon, draw(color: c, rectangle: r) is the same as draw_color_rectangle(c, r). With fake type checking, using argument name substitution, the above can be written as:

c: color = [1; 4]
r: rectangle = [0, 0, 100, 100]
draw(c, r)

A function can return a type:

fn blue() -> color {
    return [0, 0, 1, 1]
}

fn center(x, y, rad) -> rectangle {
    return [x - rad, y - rad, 2 * rad, 2 * rad]
}

c := blue() // Assigned the `color` type
r := center(100, 100, 5) // Assigned the `rectangle` type
draw(c, r)

Alternative:

draw(blue(), center(100, 100, 5))

A function can declare types for argument, but these are not used when calling the function. Therefore, to fake type checking both inside and outside, one must write the type of the argument twice:

fn add_f64_f64(a: f64, b: f64) -> f64 { return a + b }

a: f64 = 0
b: f64 = 2
c := add(a, b)

Print out stack trace and file in error messages

Currently, the file is not included in error messages.

Could print out stack trace with function names and files.

  • Add file: Arc<String> to ast::Function
  • Look up function using the call stack and put it in the error message

Type checking - Complain if proven wrong

One idea is to use type checking where it complains if there is a proof that a variable has the wrong type. This makes it possible to specify as much type information you like.

fn foo(arr: [f64], x: f64) {
    arr[0] = x
}
fn foo(arr: [f64], x: str) {
    arr[0] = x // ERROR: Expected `number`, found `string`
}

array can optionally specify the inner type, such that the following is ok:

fn foo(arr: [], x: f64) {
    arr[0] = x
}

Conversion rules:

  • [] <=> [T]
  • opt <=> opt[T]
  • res <=> res[T]

Statically local resolving

Currently, the runtime uses a loop to find the local variable declaration on the local stack.

This could be made faster by resolving the local declaration statically. Instead of a local stack, the item name is replaced by an integer that counts back relative to the end of the stack.

Result value design (`err(x)/ok(x)`)

Dyon uses a separate dynamic type for result values. The result type has two variants, err and ok. It has a lot of common with option value, but there are a few differences.

  • err(x) creates an error with message x, using deep clone
  • ok(x) creates a successful result with value x, using deep clone
  • unwrap(x) unwraps a result type, when error prints error message plus ? trace messages
  • unwrap_err(x) unwraps error message
  • is_err(x) returns true if result is an error

For both result and option, the ? operator propagates the error (requires -> on function):

  • x?, x.a?, x[0]?.b.c? etc.
  • foo()? or following after any expression

When a the value is none() or err(x), the ? operator propagates the error. Returns from the function. A trace message is added to help debugging on unwrap, describing where the ? operation happened. some(x) is converted to ok(x).

This is designed for:

  • Explicit decide whether to use result
  • Check when mutating a variable that it is result
  • Convenient for debugging
  • Common way of handling errors

Optional values design (`none()/some(x)`)

Dyon uses a separate dynamic type for optional values. The optional type has two variants, none and some.

  • none() creates an empty value
  • some(x) creates a specified value, using deep clone
  • unwrap(x) unwraps an optional value, gives an error message if none()
  • if x != none() { ... } can be used to check whether value is set

This is designed for:

  • Explicitly decide whether to use an optional value
  • Check when mutating a variable that it is optional
  • Return conveniently from functions
  • Objects can express that a field is not set, but without removing the property

Calling functions

Normal call syntax:

<name>(arg0, arg1, ...)

Named arguments sugar call syntax:

<name>(<arg0>: arg0, <arg1>: arg1, ...)

The following rules are used:

  • The named argument syntax is a sugar that adds <arg0>, <arg1> etc. to the name separated by _
  • A double underscore separates the logical name of function from arguments
  • Arguments must appear in the same order as with normal syntax
  • When an argument has a lifetime, it must be referenced as a local variable
  • The lifetimes of arguments are checked before running the program
  • The function names are checked before running the program

The rules are designed for:

  • Easy refactoring from a function with many arguments to an object
  • Calls are not affected by changes to the local argument names in the defined function
  • Use argument names as a safeguard when changing code

Use `:=` to change type in arrays

Similar to #5, but designed for arrays.

When using := in arrays, it should not allocate a new slot if the index is outside the range, since this might lead to confusing bugs. However, it can be used to overwrite the type, for example replacing a number with a string.

a = [5]

a[0] = "hello" // ERROR: Expected assigning to text

a[0] := "hello" // OK

Use draw list commands for 2D rendering in piston_window example

.draw_2d is not designed to be called for each shape. In order to avoid overhead, could use a list of objects that contained the render operations.

  • Add an external function draw which renders draw list commands
  • Add a module "render" which wraps draw list commands in an interface
  • Update piston_window such that errors can be propagated

Inspect available functions

Considering that the prelude can change for every module, Dyon should have a way to list all the available functions and their argument lifetimes. This could be added as a standard intrinsic functions().

  • List all intrinsics
  • List all externals
  • List all loaded functions
  • Sort functions by name

Named argument syntax

Example:

fn say__msg_to(msg, person) {
    print(person + "! ")
    println(msg)
}

fn main() {
    // Normal call syntax.
    say__msg_to("hi!", "you there")
    // Named argument call syntax.
    say(msg: "hi!", to: "you there")
}

Two underscores __ separates arguments from the function name. A single underscore _ separates arguments.

This syntax has the following benefits:

  • No extra information required - maps to same function
  • Can change the name of arguments without causing breaking changes
  • Fits with the Rust's snake_case for function names
  • Arguments are ordered, making it faster to parse than unordered arguments
  • Can use long function names for many arguments
  • Easy to refactor into objects

Loop syntax label and lifetimes

I see you're sticking to some rust syntax, I always found loop labels and lifetimes to be too similar at first glance. Any chance you'd reconsider the single quote for something else?

Perhaps:

@a: loop {
  break @a
}

Have you already discussed syntax? I'd enjoy reading it if so!

Add support for bools

This is missing from the language at the moment.

  • "||"
  • "&&"
  • "-" (boolean subtraction, a lazy precedence for a && !b)
  • "^" (xor)
  • "!"

`typeof`

Should be similar Javascript, except typeof([]) returns "array".

I want to use typeof(return) to check whether return value has been set. This makes it more consistent with using return as a variable.

fn typeof_return() -> {
    if typeof(return) == "return" {
        println("return OK")
    }
    return = 2
    if typeof(return) == "number" {
        println("return is number OK")
    }
    return
}

fn main() {
    if typeof("hi") == "string" {
        println("string OK")
    }
    if typeof(3) == "number" {
        println("number OK")
    }
    if typeof([]) == "array" {
        println("array OK")
    }
    if typeof({}) == "object" {
        println("object OK")
    }
    if typeof(true) == "boolean" {
        println("boolean OK")
    }
    println(typeof_return())
    return
}

This requires a special expression rule for arguments, since return would return from a function when called within a block. Here it is interpreted as a variable.

Comparing Dynamo vs Rust n_body - Round 1

$ rustc -O source/bench_rust/n_body.rs && time ./n_body

real    0m0.005s
user    0m0.001s
sys 0m0.002s

Edit source/test.rs:

fn main() {
    m := load("source/bench/n_body.rs")
    call(m, "main", [])
}
$ cargo build --release --example test && time ./target/release/examples/test

real    0m0.252s
user    0m0.247s
sys 0m0.005s

Currently, Rust is 50x faster than Dynamo.

Calling a function on a dynamic module with lifetime

module.rs:

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

loader.rs:

fn main() {
    a := {b: [0]}
    loop {
        b := [2]
        m := load("source/module.dyon")
        call(m, "lifetime_1", [a, b])
        break
    }
    println(a.b)
}

This causes unsafe behavior because the lifetime of b is not checked.

Create a library for interactive coding with Piston

Related to #71

Could live in the same repo for now to get faster iteration on Dyon functionality.

  • Create window, timers, camera controllers
  • Draw various 2D shapes
  • Handling various events
  • Helper methods for stack pushing and popping Dyon variables
  • Add custom Rust functions

Mutability design (`mut`)

Dyon uses mut to declare mutability. This appears in front of a function argument declaration or when calling a function that mutates the argument.

  • foo(mut a) mutates the argument a
  • foo(mut a) has function name foo(mut)
  • foo(mut a, b) has function name foo(mut,_)
  • foo(a, b) has function name foo

Local variables are mutable.

Mutability is not part of a type, but added to the function name inside parentheses, e.g.(mut,_). When adding an external function, the mutability information must be added as part of the function name.

Mutability propagates for arguments:

fn foo(mut a) { ... }

// `bar` requires `mut a` because it calls `foo(mut a)`.
fn bar(mut a) { foo(mut a) }

Multiple functions can use the same name if they have different mutability information. This reduces the need for "get", "set" and "mut" in function name:

fn title(window: 'return) -> { ... }

fn title(mut window, val) { ... }

t := title(window)
title(mut window, "hello world!")

This is designed for:

  • Explicitly declare when a function mutates a variable
  • Improve readability and maintenance
  • Allow a function name to be reused for different mutability patterns

∑/sum, ∏/prod, min, max, sift, ∃/any, ∀/all loops

In mathematics, you have the ∑ symbol that appears all over the place. It can be combined with a body with an arbitrary mathematical expression. Dyon uses this concept, making it easier to write programs in a more mathematical style.

The unicode symbol is be allowed in the syntax instead of the words "sum".

Example:

a := ∑ i len(list) { list[i] }

vs

sum := 0
for i len(list) {
    sum += list[i]
}

It can be used within an expression:

n := len(list)
mean := ∑ i n { list[i] } / n
geometric_mean := exp(∑ i n { ln(list[i]) } / n)

You can use continue and break, just like a normal for loop:

  • continue skips the current index
  • break skips the rest of the indices

Other loops:

  • ∏/prod (starts with 1)
  • min (starts with none())
  • max (starts with none())
  • sift (starts with [], puts result of block into an array)
  • ∃/any (returns bool, starts with false)
  • ∀/all (returns bool, starts with true)

Composition of loops

All the loops described here can be composed, which means to nest them and return a value from the inner loop:

a := min i {
        min j { ... }
    }

This can be written as a packed loop:

a := min i, j { ... }

Secrets

any, all, min and max loops, and their compositions, generate a secret. This is used to find out why or where a value was returned from the loop:

a := min i, j { ... }
println(where(a)) // prints `[i, j]` of minimum value.

For more information about secrets, see #266

Make loading module return result

When loading a module with load(source) or load_source_imports(source, imports), it should return result such that one can handle the error.

Add native Rust objects

This would allow working with native Rust types in the scripting language, for example images or a window. The objects can be shared across threads, and can be shared between a running script and a native Rust environment.

Variable::RustObject(Arc<Mutex<Any>>)

Short for loop design (`for i n { ... }`)

Dyon supports a short For loop for counters starting at 0 and incremented until it is greater or equal to a pre-evalutated expression:

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

This For loop is approximately 2.9x faster when running on AST than the equivalent traditional For loop:

n := len(list)
for i := 0; i < n; i += 1 { println(list[i]) }

Inferring range by indexing

The expression len(list) can be inferred by the index list[i] in the body:

for i { println(list[i]) }

When nesting loops this way, you have to order items in a list in the same order, for example:

for i, j, k {
    println(list[i][j][k]) // i, j, k must be used in the same order
}

However, as long as you don't depend on the previous indices, you can write it any way you like:

sum i, j {
    list[i] - list[j]
}

Specify range

With index start and end:

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

Packed loops

When nesting loops of same kind, you can pack them together by separating the indices with ",":

for i, j, k {
     println(list[i][j][k])
}

You can also write ranges in packed version, like for i n, j [i+1, n) { ... }.

Examples

Computing output for a neural network:

fn run__tensor_input(tensor: [[[f64]]], input: [f64]) -> {
    input := input
    for i {
        input = sift j {
            sigmoid(∑ k {
                tensor[i][j][k] * input[k]
            })
        }
    }
    return clone(input)
}

Compute energy for a system of N physical bodies:

fn energy(bodies: [{}]) -> f64 {
    n := len(bodies)
    return ∑ i n {
        bodies[i].vel · bodies[i].vel * bodies[i].mass / 2.0 -
        bodies[i].mass * ∑ j [i+1, n) {
            bodies[j].mass / |bodies[i].pos - bodies[j].pos|
        }
    }
}

Set all weights in a neural network to random values:

fn randomize__tensor(mut tensor: [[[f64]]]) {
    for i, j, k {
        tensor[i][j][k] = random()
    }
}

Motivation

This is designed for:

  • Reduce typing
  • Reduce bugs
  • Improve readability of code

Make `root` a fixed global variable

A technique I use is to reload a script every nth second. To do this safely I need to call the "main" function for every event.

fn main() {
    if render() {
        // Do rendering.
    }
    if update() {
        // Do update.
    }
}

Since Runtime::stack is separated from the module, it is possible to share state through the stack.

Since variables runs out of scope, there is no state. I tried hacking this using a custom Rust function, but it doesn't work with the current assignment rules (perhaps a good thing). The reason it doesn't work is references are only resolved one step. Assigning to Variable::Ref(0) means a reference to a reference, which does not get resolved.

One idea is to push an object at the bottom of the stack and call it "root". This object is available everywhere and can be used to store global settings and share state.

  • := should not be allowed with the "root" object
  • = should work whenever the left side is "root"
  • "root" outlives all other variables

Make `clone` instrinsic a deep clone

Currently, a clone(a) where a contains internal references is unsafe.

The idea is that clone always should returns a copy of all data it contains, such that you can use it when you do not want to use lifetimes.

Unused function causes unsafe behavior

fn one() -> {return 1} // commenting this function triggers correct lifetime check
fn bar(x: 'return) -> {return [x]}
fn foo() -> {
    return bar(0)
}
fn main() {
    println(foo())
}

This happens only when the function is first.

Assignment

Declaration syntax:

<left> := <right>

Mutable assignment syntax:

<left> ?= <right>

Where ?= can be =, +=, *= etc.

The following rules are used:

  • := does a shallow clone of right value, if it is a reference
  • = does a shallow clone of right value
  • := ignores the type of the assigned item
  • ?= checks the type of assigned item
  • ?= does a shallow clone (copy-on-write) of left value before assigning, if it is a reference

The rules are designed for:

  • Make numbers and booleans feel natural without the runtime distinguishing between copy and non-copy variables
  • Allow sharing of objects, but with the ability to replace it with a non-shared object
  • Copy-on-write behavior, where no simple assignment mutates an object elsewhere
  • Avoid making variables entangled in surprising ways
  • Prevent infinite cycles of references

Make a demo where a module is reloaded every nth second

This could be part of the piston_window example. The purpose of this demo is to show how dynamic modules can be used to code on the fly. Currently the piston_window example does this in a hard coded way, but I would like to have this flexibility in Dyon itself.

Add `Variable::Option` and `Variable::Result`

Objects can have optional fields and functions can return errors. Some syntax sugar could make this convenient to use, specially for propagating errors.

  • Instead of adding/removing fields to an object, one can use Variable::Option
  • Run-time type checking of mutating a variable prevents errors, for example replacing a number with None
  • Standard way of dealing with errors, making it easier to share code
  • some(x), none, ok(x) and err(x) can be built-in functions

Return without a value

Currently, this does not work:

fn main() {
    for i := 0; i < 100; i += 1 {
        if i > 14 {
            return
        }
        println(i)
    }
}

Type syntax design

Dyon uses an optional type system, one that complains if the types are proven to be wrong.

  • bool (boolean)
  • f64 (number)
  • vec4 (4D vector)
  • []/[T] (array)
  • {} (object)
  • opt[T] (option)
  • res[T] (result)
  • thr[T] (thread)
  • any - no syntax keyword - (any type, except void)
  • void - no syntax keyword - (no value type)

Example

fn foo() -> opt[bool] {
    return some(true)
}

fn main() {
    if foo() {
        println("oh?")
    }
}
 --- ERROR --- 
In `source/test.rs`:
Type mismatch: Expected `bool`, found `opt[bool]`
6,8:     if foo() {
6,8:        ^

Any

An any type works with any other type except void.

Non-guarantee of argument type inside function body

Consider a simple example like this:

fn foo(a: bool) { ... }

It is not guaranteed that a has the type bool inside the function body. This is because the function can be called with an argument that is inferred to any at type checking.

Motivation

These rules are designed for:

  • Fast coding without having to fill out all the types
  • Catch errors earlier when changing Rust/Dyon interface
  • Backward compatible with old code when developing more advanced type checking later

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.