Getting started

To get started with gluon we must first have a way to compile and run Gluon programs. The fastest way to get that is to download one of the pre-built binaries for Linux, Windows, OSX or FreeBSD from https://github.com/gluon-lang/gluon/releases. Alternatively, if you have a Rust compiler and Cargo installed you may install it with cargo install gluon_repl.

Once installed you can verify that it works by saving the following program into a file named hello_world.glu and then compile and run it with gluon hello_world.glu.

let io = import! std.io
io.println "Hello world!"

If everything works the program should have printed Hello world! to your terminal.

Dissecting Hello World

let io = import! std.io
io.println "Hello world!"

There are a number of things going on in the hello world example so lets break them down one step at a time.

let

Gluon uses the keyword let to bind values for later use.

import! std.io

import! is a builtin macro which loads the contents of another module. In the example we use it to get access to the standard library's io module. Since it appeared on the right side of let io = we thus bound the std.io module to the io binding.

io.println

Here we access the println function from the io module we bound earlier which is a function that lets us write strings to stdout.

"Hello world!"

Finally we create a string literal which gets passed to println to get printed.

Using the REPL

Though it is possible to continue running any programs by saving it to a file and running it with gluon my_script.glu there is an easier way to go about it when you want to experiment quickly with small programs. By running gluon -i, gluon starts in "interactive" mode, giving you a REPL where you may evaluate expressions and inspect their results. Try evaluating some simple arithmetic expressions to see that it works.

> 1 + 2
3
> 100 * 3 + 4
304
> 3.14 * 10.0
31.400000000000002

Evaluating only a single expression can get quite unwieldy so if we want to break something up into multiple steps we can use let to give a name to an expression.

> let pi_2 = 3.14 * 2.0
6.28
> pi_2 * 3.0
18.84

These are the basic parts of the REPL and if you want to you can try writing hello world again by using the features above.

If you still have the hello_world.glu file around there is another way to run it from inside the REPL by using the special :script (:s) command.

> :s hello_world.glu
Hello World!

There are a few other of these special commands as well and you can find them all with :help (:h).

> :type 1
Int
> :info std.io.println
std.io.println: String -> IO ()
> :kind std.option.Option
Type -> Type

Finally you may quit the REPL using the :quit (:q) command or using <CTRL-D>.

Anatomy of a gluon program (draft)

Let's look at a slightly larger program by writing a guessing game. In this game the player will guess at a random number between 1 and 100 and the program will say whether each guess is to low or to high. If the player guesses correctly the program will congratulate the player and exit.

As a base we can take the hello world example.

let io = import! std.io
io.println "Hello world!"

The first thing we will need is a way to get a number from the user. For this we will use the std.io.read_line : IO String action. To test that it works we will simply write the line back to the user.

let io @ { ? } = import! std.io
do line = io.read_line
io.println line

There are two new concepts in play here, implicit arguments and do expressions.

do expressions are similar to let expressions in that they let us us bind the result of an expression to a name which we can use later. Where they differ is that, rather binding the result of evaluating the expression itself, they expect the right hand side to be a monadic action such as IO and the value bound is the result of evaluating the action, which in this case is the String that that were input.

(As was alluded to in the previous paragraph IO is a Monad, a rather complex concept which I won't go into here as it is enough for our purposes to only consider the "IO monad" as something that describes how IO actions are run in sequence.)

do expressions don't just magically work with IO actions which is where implicit arguments come in, it lets us use the compiler to to implicitly insert certain function arguments by looking at the inferred types. This can be thought as a way to get something similar to traits in Rust but with a bit extra flexibility by requiring a bit of explicitness to let the compiler know what it can use as an implicit argument. Which is why we needed to add the { ? } record pattern match, the ? lets the compiler know that it should choose from the fields of the record when looking for an implicit argument. In this case the compiler sees that we use IO in the do expression and implicitly inserts an implicit IO Monad value found in std.io, letting the do expression know how to sequence these two actions.

Syntax and semantics

Gluon is a functional language at heart, basing its syntax on languages such as F#, OCaml and Haskell. The syntax may thus look strange if you are coming from C-like languages but don't be discouraged! There is actually very little syntax to learn.

If, on the other hand, you are familiar with functional languages you will be right at home. Roughly speaking, Gluon takes the expression syntax from F# and OCaml and uses the type syntax of Haskell.

Identifiers and Literals

The simplest syntactical elements in Gluon are identifiers and literals and none of them should be especially surprising if you are experienced in programming.

Identifiers are a sequence of alphanumeric characters including underscore ("_") which are required to start with either a letter or an underscore. Literals come in four different forms - integer, float, string and character literals.

// An identifier
abc123_

// An integer literal

42
// A float literal
3.14

// A string literal
"Hello world"

// A raw string literal
r"Can contain newlines
world"
r#"With # as delimiters raw strings can also contain quotes without escaping `"` "#
r###" "## "###

// A character literal
'e'

Comments

Comments should be immediately familiar if you are accustomed to C-like languages.

// starts a line comment which is ended by a newline

/* starts a block comment which is ended by */

Functions

f x "argument" 3

Being a functional language, functions are everywhere. Because of this, calling functions have an intentionally minimalistic syntax where there is no need to enclose arguments as a parenthesized list of arguments. Instead, arguments are separated by whitespace.

Another way of calling a function is through infix notation since gluon implements all operators as just functions.

1 + 2 // Calls the + function on 1 and 2
(+) 1 2 // Parenthesizing an operator makes it possible to use in a normal function call

It is important to note that function application binds harder than any binary operator.

(+) 0 1 - (+) 2 3 // Equivalent to (0 + 1) - (2 + 3)

Variable bindings

Any language more complex than Hello world is bound to require variable bindings which serve to bind some value to a name allowing it to be used later.

let x = 1 + 2 in x // Returns 3

You may rightly be wondering about the in x part. gluon takes a strong stance against statements in an effort to keep things consistent. Thus only writing let x = 1 + 2 will be met with a syntax error about a missing in keyword which is what defines the actual value returned from the let expression.

Let bindings also allow functions to be defined which is done by listing the arguments between the bound identifier and =

// Defines the `id` function which takes a single argument and returns it.
let id x = x in id 1 // Returns 1

Mutually recursive values can be defined using rec ... in to enclose the let bindings.

rec
let f x = g x
let g x = f x
in f 1 // Never returns

This is not limited to functions but works with any value that is capable of recursion (records, variants and functions).

/// An infinite list of `1`
rec let ones = Cons 1 ones
in

/// A recursive set of records
rec
let value1 =
    let f x = value2.f x + 1
    { f }
let value2 =
    let f x = value1.f x + 2
    { f }
in ()

If expressions

The simplest control flow expression is the if expression. It evaluates a boolean expression, taking the first branch if the boolean evaluates to True, and taking the second if it evaluates to False

if True then 1 else 0

Record expressions

To create more complex data types, Gluon has first class records. Records can be used to couple data that is logically grouped into a single type.

{ pi = 3.14, add1 = (+) 1.0 }

To access the fields of a record, . is used.

let record = { pi = 3.14, add1 = (+) 1.0 }
in record.pi // Returns 3.14

Field assignments can be omitted if there is a variable in scope with the same name as the field.

let id x = x
in { id }

The .. operator can be used at the end of a record expression to take all fields of one record and fill the constructed record. Explicitly defined fields that also exist in the base record will be in the same order as they are in the base record while all other fields will be prepended in the order that they are written.

let base_record = { x = 1, y = 2, name = "gluon" }
in
// Results in a record with type
// { field : Bool, x : Int, y : Int, name : String }
{
    field = True,
    ..
    base_record
}

Array expressions

Arrays can be constructed with array literals.

// Results in an `Array Int`
[1, 2, 3, 4]

Since Gluon is statically typed all values must be of the same type. This allows the Gluon interpreter to avoid tagging each value individually which makes types such as Array Byte be convertible into Rust's &[u8] type without any allocations.

// ERROR:
// Types do not match:
//        Expected: Int
//        Found: String
[1, ""]

Functions to operate on arrays can be found on the array module.

array.len [1, 2, 3]

Variants

While records are great for grouping related data together, there is often a need to have data which can be one of several variants. Unlike records, variants need to be defined before they can be used.

type MyOption a = | Some a | None
Some 1

Match expressions

To allow variants to be unpacked so their contents can be retrieved, Gluon has the match expression.

match None with
| Some x -> x
| None -> 0

Here, we write out a pattern for each of the variant's constructors and the value we pass in (None in this case) is matched to each of these patterns. When a matching pattern is found, the expression on the right of -> is evaluated with each of the constructor's arguments bound to variables.

match expressions can also be used to unpack records.

match { x = 1.0, pi = 3.14 } with
| { x = y, pi } -> y + pi

// Patterns can be nested as well
match { x = Some (Some 123) } with
| { x = Some None } -> 0
| { x = Some (Some x) } -> x
| { x = None } -> -1

let bindings can also match and unpack on data but only with irrefutable patterns. In other words, only with patterns which cannot fail.

// Matching on records will always succeed since they are the only variant
let { x = y, pi } = { x = 1.0, pi = 3.14 }
in y + pi

// These will be rejected however as `let` can only handle one variant (`Some` in this example)
let Some x = None
let Some y = Some 123
x + y

Tuple expressions

Gluon also have tuple expressions for when you don't have sensible names for your fields.

(1, "", 3.14) // (Int, String, 3.14)

Similarily to records they can be unpacked with match and let.

match (1, None) with
| (x, Some y) -> x + y
| (x, None) -> x

let (a, b) = (1.0, 2.14)
a + b

Infact, tuples are only syntax sugar over records with fields named after numbers (_0, _1, ...) which makes the above equivalent to the following code.

match { _0 = 1, _1 = None } with
| { _0 = x, _1 = Some y } -> x + y
| { _0 = x, _1 = None } -> x

let { _0 = a, _1 = b } = { _0 = 1.0, _1 = 2.14 }
a + b

While that example is obviously less readable the tuple syntax, the important thing to note is that tuples equivalency with records allows one to access the fields of a tuple directly without unpacking.

(0, 3.14)._1 // 3.14

Lambda expressions

While we have seen that functions can be defined in let expressions it is often valuable to define a function without giving it an explicit name.

// \(<identifier)* -> <expr>
\x y -> x + y - 10
// Equivalent to
let f x y = x + y - 10 in f

Type expressions

Gluon allows new types to be defined through the type expression which, just like let, requires in <expression> to be written at the end to ensure it returns a value.

// type <identifier> <identifier>* = <type> in <expression>
type MyOption a = | None | Some a
let divide x y : Int -> Int -> MyOption Int =
    if (x / y) * y == x then
        Some (x / y)
    else
        None
in divide 10 4

An important difference from many languages however is that type only defines aliases. This means that all types in the example below are actually equivalent to each other.

type Type1 = { x: Int }
type Type2 = Type1
type Type3 = { x: Int }
let r1 : Type1 = { x = 0 }
let r2 : Type2 = r1
let r3 : Type3 = r2
in r1

Mutually recursive types can be defined by writing a rec block.

rec
type SExpr_ = | Atom String | Cons SExpr SExpr
type SExpr = { location: Int, expr: SExpr_ }
in Atom "name"

Do expressions

do expressions are syntax sugar over the commonly used Monad type which is used to encapsulate side-effects. By using do instead of >>= or flat_map we can write our code in a sequential manner instead of the closures necessary for sugar free versions. Note do still requires a flat_map binding to be in scope with the correct type or else you will get an error during typechecking.

Some 1 >>= (\x -> Some (x + 2))
// or
flat_map (\x -> Some (x + 2)) (Some 1)

// are equivalent to

do x = Some 1
Some (x + 2)

Indentation

If you have been following along this far, you may be think that the syntax so far is pretty limiting. In particular, you wouldn't be wrong in thinking that the let and type syntax are clunky due to their need to be closed by the in keyword. Luckily, Gluon offers a more convenient way of writing bindings by relying on indentation.

When a token starts on the same column as an unclosed let or type expression, the lexer implicitly inserts an in token which closes the declaration part and makes the following expression into the body.

let add1 x = x + 1
add1 11 // `in` will be inserted automatically since `add1 11` starts on the same line as the opening `let`

If a token starts on the same column as an earlier expression, but there is not an unclosed type or let expression, Gluon treats the code as a block expression, meaning each expression is run sequentially, returning the value of the last expression.

do_something1 ()
do_something2 () // `do_something1 ()` is run, then `do_something_2`. The result of `type ...` is the result of the expression
type PrivateType = | Private Int
let x = Private (do_something3 ())
do_something3 ()
match x with
| Private y -> do_something4 x

Indented blocks can be used to limit the scope of some variables.

let module =
    let id x = x
    type MyInt = Int
    { MyInt, id, pi = 3.14 }

module.id module.pi

Which is equivalent to:

let module =
    let id x = x
    in
    type MyInt = Int
    in { MyInt, id, pi = 3.14 }
in
module.id module.pi

Typesystem

In gluon, identifiers starting with an uppercase letter is a type whereas identifiers starting with a lowercase letter are type variables.

Function types

<type> -> <type>

Function types are written using the (->) operator, which is right associative. This means that the function type Int -> (Int -> Int) (A function taking one argument of Int and returning a function of Int -> Int) can be written as Int -> Int -> Int.

Record type

type_identifier := [A-Z][A-Za-z_0-9]*
variable_identifier := [a-z][A-Za-z_0-9]*

field := <type_identifier> <variable_identifier>* = <type>
       | <type_identifier>
       | <variable_identifier> : <type>

record_type := { (field,)* }

// Example
{
    Float,
    BinaryOp = Float -> Float -> Float,
    pi : Float,
    sin : Float -> Float
}

Records are Gluon's main way of creating associating related data and they should look quite familiar if you are familiar with dynamic languages such as javascript. Looks can be deceiving however as gluon's records are more similar to a struct in Rust or C as the order of the fields are significant, { x : Int, y : String } != { y : String, x : Int }. Furthermore, records are immutable, meaning fields cannot be added nor removed and the values within cannot be modified.

In addition to storing values, records also have a secondary function of storing types which is Gluon's way of exporting types. If you have used modules in an ML language, this may look rather familiar. Looks can be deceiving however as 'type fields' must match exactly in gluon which means there is no subtyping relationship between records ({ Test = { x : Int } } is not a subtype of { Test = Float }). This may change in the future.

{ Test = { x : Int } }

Enumeration type

( | <identifier> (<type>)* )*

| Err e | Ok t

Gluon also has a second way of grouping data which is the enumeration type which allows you to represent a value being one of several variants. In the example above is the representation of Gluon's standard Result type. It represents either the value having been successfully computed (Ok t) or that an error occurred (Err e).

Alias type

<identifier> (<type>)*
Int
Float
Option Int
Ref String

The last kind of type which Gluon has is the alias type. An alias type explicitly names some underlying type which can either be one of the three types mentioned above or an abstract type which is the case for the Int, String and Ref types. If the underlying type is abstract, then the type is only considered equivalent to itself (ie if you define an abstract type of MyInt which happens to have the same representation as Int the typechecker will consider these two types as being distinct).

Higher-kinded types

Higher-kinded types are a fairly abstract concept in Gluon and you may create entire programs without any knowledge about them. Sometimes they are a very valuable tool to have, as they can be used to create very powerful abstractions.

Just as all values such as 0 : Int, "Hello World!" : String and Some 4.0 : Option Float each have a type, these types themselves have their own 'type' or the 'kind' as it is called. For the types of concrete values the Kind is always Type so for the earlier examples Int : Type, String : Type and Option Float : Type. That is not very useful on its own but it becomes more interesting when we consider the kind of Option : Type -> Type. Type -> Type looks rather like the type of a function such as show_int : Int -> String but, instead of taking a value, it takes a type and produces a new type. In effect, this lets us abstract over types instead of just over values. This abstraction facility can be seen in the Functor : (Type -> Type) -> Type type which takes a type with kind Type -> Type as argument which is exactly the kind of Option (or List, Result a).

Universal quantification

First draft

Universal quantification is what Gluon's "generic types" are called. Consider the identity function in Rust.

fn id<T>(x: T) -> T {
    x
}

In Gluon the same function would be written in the following way if it were fully annotated.

let id x : forall a . a -> a = x
// Types can of course be omitted in which the same type as above will be inferred
let id x = x
// Unbound type variables (`a` in this example) are allowed, in which case a `forall` will be
// inserted at the at the "top" of the type (same place as the type above)
let id x : a -> a = x

So in simple case, forall is no different from declaring type parameters to a function in Rust. But forall also serves more advanced use cases and is at the center when it comes to making Gluon's records work as modules.

let module =
    let id x = x

    { id }

module.id 0
module.id ""

If we were to emulate the above code in Rust we would probably end up with something like this code.

struct Module<T> {
    id : Box<Fn(T) -> T>,
}

let module = Module {
    id: Box::new(|x| x),
};
(module.id)(0);
(module.id)("");

Alas, this does not work in Rust since module will be inferred to the type Module<i32> which makes the second call to id a type error. In gluon it works as the type of module is actually { id : forall a . a -> a } and not forall a . { id : a -> a } which is the closest analogue to the Rust example.

Intuitively, we can say that since gluon lets forall be specified inside types we can avoid specializing the type (in this case forall a . a -> a) which lets us specialize module.id once for each call to id instead of specializing the entire module at once.

While all of this looks quite complex, it should for the most part not matter when writing code and common idioms will just work as expected!

Implicit arguments

Sometimes, there is a need to overload a name with multiple differing implementations and let the compiler chose the correct implementation. If you have written any amount of Gluon code so far, you are likely to have already encountered this with numeric operators such as (+) or comparison operators such as (==). If you inspect the types of these functions you will find that the first argument of these functions look a little bit different from normal functions.

(==): : forall a . [std.prelude.Eq a] -> a -> a -> std.types.Bool
(+): forall a . [std.prelude.Num a] -> a -> a -> a

This different looking argument is an implicit argument which means that you do not need to pass a value for this argument, instead, the compiler will try to find a value with a type that matches the type signature. So if you were to call 1 == 2 the compiler will see that the type variable a has been unified to Int. Then when the implicit argument is resolved it will look for a value with the type Eq Int.

Since searching all possible bindings currently in scope would introduce to many ambiguity errors the compiler does not search all bindings when trying to determine an implicit argument. Instead, whether a binding is considered for implicit resolution is controlled by the #[implicit] attribute. When marking a let binding as #[implicit] and this binding is in scope it will be considered as a candidate for all implicit arguments. The #[implicit] attribute can also be set on a type binding in which case it applied to all let bindings which has the type declared by the type binding.

#[implicit]
type Test = | Test ()
let f y: [a] -> a -> a = y
let i = Test ()
// `i` gets selected as the implicit argument since `#[implicit]` is marked on the type and `i : Test`
()

Since importing each individual binding used as an implicit argument quickly becomes tedious there is a short-hand to bring all implicit bindings from a record into scope.

let { eq, ord } = import! std.int
1 == 1 && 1 < 2
// Also brings in `show`, `num` ...
let { ? } = import! std.int
1 == 1 && 1 < 2

For standard types such as Int, Float, String, Bool and Option this gets injected through the implicit prelude that is inserted before all code which lets ==, < etc to work out of the box.

Passing implicit arguments explicitly

If you only use implicit functions as explained above then it might just seem like a different name for traits (Rust) or type classes (Haskell). While it is true that the main reason for implicit arguments is to emulate traits/type classes implicit arguments is more powerful than those approaches as it is also possible to override the implicit resolution and instead give the argument explicitly by prefixing the argument with ?.

let list @ { List } = import! std.list
// Make a custom equality function which returns true regardless of the elements of the list
#[infix(left, 4)]
let (===) = (list.eq ?{ (==) = \x y -> True }).(==)
Cons 1 (Cons 2 Nil) === Cons 3 (Cons 4 Nil)

The inverse also works when defining a function with implicit arguments. By prefixing an argument by ? an implicit arguments will be given a name inside the function (if ? is not given in a function definition the argument will only be available for implicit resolution).

let eq ?a : [Eq a] -> Eq (Option a) = {
    (==) = \l r ->
        match (l, r) with
        | (Some l_val, Some r_val) -> a.(==) l_val r_val
        | (None, None) -> True
        | _ -> False,
}
()

Importing modules

As is often the case, it is convenient to separate code into multiple files which can later be imported and used from multiple other files. To do this, we can use the import! macro which takes a single string literal as argument and loads and compiles that file at compile time before the importing module is compiled.

For example, say that we need the assert function from the test module which can be found at std/test.glu. We might write something like this:

let { assert } = import! std.test
assert (1 == 1)

Writing modules

Importing standard modules is all well and good but it is also necessary to write your own once a program starts getting too big for a single file. As it turns out, if you have been following along so far, you already know everything about writing a module! Creating and loading a module in gluon entails creating a file containing an expression which is then loaded and evaluated using import!. import! is then just the value of the evaluated expression.

// module.glu
type Named a = { name: String, value: a }
let twice f x = f (f x)
{ twice, Named }

//main.glu
let { twice, Named }  = import! "module.glu"
let addTwice = twice (\x -> x + 1)
let namedFloat : Named Float = { name = "pi", value = 3.14 }
addTwice 10

Though modules are most commonly a record, this does not have to be the case. If you wanted, you could write a module returning any other value as well.

// pi.glu
3.14

//main.glu
let pi  = import! "pi.glu"
2 * pi * 10

Metadata

Sometimes we need a way to associate some extra information to a specific named binding and have it be visible whenever we refer to that name. The most common reason for this is documentation comments. When we write some documentation for a binding we would like this documentation to be visible whenever someone uses that binding.

For this reason gluon runs a "metadata" pass on all code in which "metadata" (such as documentation comments) gets statically propagated throughout the code.

/// Adds one to the argument `x`
let add1 x = x + 1

/// Adds two to the argument `x`
let add2 x = x + 2

add1 // Looking up the metadata of this variable yields the documentation of `add1`

// It can't be statically determined which branch the `if` takes (since constant folding do not
// take place). Thus `addN` do not get any metadata from either `add1` or `add2`
let addN = if True then add1 else add2
addN

Attributes

In addtion to documentation comments gluon also has a special notion of attributes that get propagated in the same manner. These are specified using the following syntax.

Attribute : #[ AttributeContents ]

AttributeContents :
      #[ IDENTIFIER ]
    | #[ IDENTIFIER ( TOKENS* ) ]

#[infix(..)]

#[infix(<left|right>, <NON-NEGATIVE INTEGER>)]

The #[infix] attribute is used to specified the fixity and precedence of infix operators. This lets us specify that multiplication binds tighter that addition.

#[infix(left, 6)]
let (+) ?num : [Num a] -> a -> a -> a = num.(+)
#[infix(left, 7)]
let (*) ?num : [Num a] -> a -> a -> a = num.(*)

#[implicit]

#[implicit]

The #[implicit] attribute is used to mark value bindings or type bindings as usable for implicit resolution. If specified on a value binding then only that specific binding can be used on implicit resolution. If specified on a type binding then all bindings that has that type can be used in implicit resolution.

// Can be used as an implicit argument
#[implicit]
let binding : MyType = ..

#[implicit]
type Eq a = { (==) : a -> a -> Bool }

// Can be used as an implicit argument
let eq_Int : Eq Int = ..

#[derive(..)]

#[derive(IDENTIFIER)]

The #[derive(..)] attribute can be used on type bindings to generate implementations for some traits. Currently only Eq and Show can be derived and only non-recursive and self-recursive types are supported (mutually recursive types do not work for the moment).

#[derive(Eq, Show)]
type Tree a = | Tip a | Branch (Tree a) (Tree a)

let tree = Branch (Tip 1) (Branch (Tip 2) (Tip 3))

show tree
tree == Tip 1

#[doc(hidden)]

#[doc(hidden)]

The #[doc(hidden)] attribute hides the binding, omitting it from generated documentation.

Modules

Importing modules

As is often the case, it is convenient to separate code into multiple files which can later be imported and used from multiple other files. To do this, we can use the import! macro which takes a single string literal as argument and loads and compiles that file at compile time before the importing module is compiled.

For example, say that we need the assert function from the test module which can be found at std/test.glu. We might write something like this:

let { assert } = import! std.test
assert (1 == 1)

Writing modules

Importing standard modules is all well and good but it is also necessary to write your own once a program starts getting too big for a single file. As it turns out, if you have been following along so far, you already know everything about writing a module! Creating and loading a module in gluon entails creating a file containing an expression which is then loaded and evaluated using import!. import! is then just the value of the evaluated expression.

// module.glu
type Named a = { name: String, value: a }
let twice f x = f (f x)
{ twice, Named }

//main.glu
let { twice, Named }  = import! "module.glu"
let addTwice = twice (\x -> x + 1)
let namedFloat : Named Float = { name = "pi", value = 3.14 }
addTwice 10

Though modules are most commonly a record, this does not have to be the case. If you wanted, you could write a module returning any other value as well.

// pi.glu
3.14

//main.glu
let pi  = import! "pi.glu"
2 * pi * 10

Embedding API

The API with which the host language interacts with Gluon is very important part of the library. While the complete API can be found in the Rustdoc, this section will explain the most important parts. Please note that the API can change at any point and there are still some public functions which should actually be internal.

Creating a virtual machine

Before you are able to do anything with the library, you will need to create a virtual machine. The virtual machine is responsible for running Gluon programs and can be created with the new_vm function.

Compiling and running gluon code

Once in possession of a RootedThread, you can compile and execute code using the run_expr method on the [Compiler][] builder type.

let vm = new_vm();
let (result, _) = Compiler::new()
    .run_expr::<i32>(&vm, "example", "1 + 2")
    .ok();
assert_eq!(result, Some(3));

Notably, if we were to execute a script with side effects the code above will actually not run the side effects. To make gluon run side effects we need to set the run_io flag on [Compiler][].

let vm = new_vm();

let script = r#"
let io = import! std.io
io.print "123"
"#;
// Returns an action which prints `123` when evaluated
Compiler::new()
    .run_expr::<IO<()>>(&vm, "example", script)
    .unwrap();
// Prints `123` to stdout
Compiler::new()
    .run_io(true)
    .run_expr::<IO<()>>(&vm, "example", script)
    .unwrap();

Often, it is either inconvenient or inefficient to compile and run code directly from source code. To write the above example in a more efficient way, we could instead load the (+) function and call it directly.

let vm = new_vm();
// Ensure that the prelude module is loaded before trying to access something from it
Compiler::new()
    .run_expr::<OpaqueValue<&Thread, Hole>>(&vm, "example", r#" import! std.prelude "#)
    .unwrap();
let mut add: FunctionRef<fn (i32, i32) -> i32> = vm.get_global("std.prelude.num_Int.(+)")
    .unwrap();
let result = add.call(1, 2);
assert_eq!(result, Ok(3));

Calling Rust functions from gluon

Gluon also allows native functions to be called from gluon. To do this we first need to define the function so it is available when running Gluon code.

fn factorial(x: i32) -> i32 {
    if x <= 1 {
        1
    } else {
        x * factorial(x - 1)
    }
}

fn load_factorial(vm: &Thread) -> vm::Result<vm::ExternModule> {
    vm::ExternModule::new(vm, primitive!(1, factorial))
}

let vm = new_vm();

// Introduce a module that can be loaded with `import! factorial`
add_extern_module(&vm, "factorial", load_factorial);

let expr = r#"
    let factorial = import! factorial
    factorial 5
"#;

let (result, _) = Compiler::new()
    .run_expr::<i32>(&vm, "factorial", expr)
    .unwrap();

assert_eq!(result, 120);

add_extern_module can do more than just exposing simple functions. For instance, the primitives module export large parts of Rust's string and float modules directly as records in Gluon under the str and float modules respectively.

let vm = new_vm();
let (result, _) = Compiler::new()
    .run_expr::<String>(&vm, "example", " let string  = import! \"std/string.glu\" in string.trim \"  Hello world  \t\" ")
    .unwrap();
assert_eq!(result, "Hello world");

Marshalling types

An important part of embedding Gluon is translating non-primitive types from Gluon types to Rust types and vice versa, allowing you to seamlessly implement rich APIs with complex types. This translation is called marshalling.

Required traits

Gluon provides several traits for safely marshalling types to and from Gluon code:

  • VmType provides a mapping between Rust and Gluon types. It specifies the Gluon type the implementing Rust type represents. All types that want to cross the Gluon/Rust boundary must implement this trait.

  • Getable: Types that implement Getable can be marshalled from Gluon to Rust. This means you can use these types anywhere you are receiving values from Gluon, for example as parameters for a function implemented on the Rust side or as return type of a Gluon function you want to call from Rust.

  • Pushable is the counterpart to Getable. It allows implementing types to be marshalled to Gluon. Values of these types can returned from embedded Rust functions and be used as parameters to Gluon functions.

  • Userdata allows a Rust type to be marshalled as completely opaque type. The Gluon code will be able to receive and pass values of this type, but cannot inspect it at all. This is useful for passing handle-like values, that will be mostly used by the Rust code. Pushable is automatically implemented for all types that implement Userdata. Getable is automatically implemented for &T where T: Userdata when used as argument to a Rust function, for places OpaqueValue can be used as a smart pointer around a Userdata value or the UserdataValue extractor can be used to clone the value.

Gluon already provides implementations for the primitive and common standard library types.

Implementing the marshalling traits for your types

You can implement all of the above traits by hand, but for most cases you can also use the derive macros in gluon_codegen.

You will also have to register the correct Gluon type. If you are marshalling Userdata, you can use Thread::register_type, otherwise you will need to provide the complete type definition in Gluon. When using the serialization feature, you can automatically generate the source code using the api::typ::make_source function.

Using derive macros

Add the gluon_codegen crate to your Cargo.toml and import it:

#[macro_use]
extern crate gluon_codegen;

VmType, Getable and Pushable can be derived for types that consist only of types that already implement the respective traits. In the case of VmType you also have to specify the Gluon type the Rust type maps to, using the #[gluon(vm_type = "<gluon_type>")] attribute.

Userdata can be derived for any type as long as it is Debug + Send + Sync and has a 'static lifetime.

Implementing by hand

The following examples will all assume a simple struct User<T>, which is defined in a different crate (You can find the full code in the marshalling example). To implement the marshalling traits, we have to create a wrapper and implement the traits for it.

// defined by a different crate
struct User<T> {
    name: String,
    age: u32,
    data: T,
}

VmType

VmType requires you to specify the Rust type that maps to the correct Gluon type. You can simply assign Self. The heart of the trait is the make_type function. To get the correct Gluon type, you will have to look it up from the vm, using the fully qualified type name:

let ty = vm.find_type_info("examples.wrapper.User")
    .expect("Could not find type")
    .into_type();

If you have a non generic type, this is all you need. In our case, we will have to apply the generic type parameters first:

let mut vec = AppVec::new();
vec.push(T::make_type(vm));
Type::app(ty, vec)

You simply push all parameters to the AppVec in the order of their declaration, and then use Type::app to construct the complete type.

Getable

Getable only has one function you need to implement, from_value. It supplies a reference to the vm and the raw data, from which you have to construct your type. Since we are implementing Getable for a complex type, we are only interested in the ValueRef::Data variant.

let data = match data.as_ref() {
    ValueRef::Data(data) => data,
    _ => panic!("Value is not a complex type"),
};

From data we can now extract the individual fields, using lookup_field for named fields or get_variant for unnamed fields (like in tuple structs or variants).

// once we have the field's value, we construct the correct type
// using its Getable implementation
let name = String::from_value(vm, data.lookup_field(vm, "name").unwrap();

In this example we used a struct, but if we wanted to construct an enum, we need to find out what variant we are dealing with first, using the tag method:

match data.tag() {
    0 => // build first variant
    1 => // build second variant
    // ...
}

Pushable

To implement Pushable, we need to interact with Gluon's stack directly. The goal is to create a Value that represents our Rust value, and push it on the stack. In order to do that, we need to push the fields of our type first:

self.inner.name.push(vm, ctx)?
self.inner.age.push(vm, ctx)?;
self.inner.data.push(vm, ctx)?;

The ActiveThread we get passed has a Context that allows pushing values, but we can do even better and use the record! macro:

(record!{
    name => self.inner.name,
    age => self.inner.age,
    data => self.inner.data,
}).push(ctx)

If we were pushing an enum, we would have to use Context::push_new_data and manually specify the tag of the pushed variant as well as its number of fields (zero if it's a variant with no attached data).

let val = match an_enum {
    Enum::VariantOne => ctx.context().push_new_data(vm, 0, num_fields_in_variant_one),
    Enum::VariantTwo => ctx.context().push_new_data(vm, 1, num_fields_in_variant_two),
}?;

Userdata

Implementing Userdata is straight forward: there are no required methods, so we can simply use the default implementation. However, Userdata also requires the type to implement VmType and Traverseable. We can use the minimal VmType implementation, it already provides the correct make_type function for us:

impl<T> VmType for GluonUser<T>
where
    T: 'static + Debug + Sync + Send
{
    type Type = Self;
}

In our case, the Traverseable implementation can also be left empty. Only if a type contains types that are managed by Gluon's GC, it is necessary to implement the traverse method in order to call traverse on those types. This way the GC can find all the value it manages.

// empty impl for 'normal' types
impl<T> Traverseable for GluonUser<T> {}

// for a type that contains a Traversable type, we need to call
// its traverse method
impl Traverseable for ContainsGcPtr {
    fn traverse(&self, gc: &mut Gc) {
        self.gc_ptr.traverse(gc);
    }
}

Passing values to and from Gluon

Once your type implements the required traits, you can simply use it in any function you want to expose to Gluon.

If you want to receive or return types with generic type parameters that are instantiated on the Gluon side, you can use the Opaque type together with the marker types in the generic module:

// we define Either with type parameters, just like in Gluon
#[derive(Getable, Pushable, VmType)]
#[gluon(vm_type = "examples.either.Either")]
enum Either<L, R> {
    Left(L),
    Right(R),
}

// the function takes an Either instantiated with the `Opaque` struct,
// which will handle the generic Gluon values for us
use gluon::vm::api::OpaqueValue;
// The `generic` sub-module provides marker types which mimic generic parameters
use gluon::vm::api::generic::{L, R};

fn flip(
    either: Either<OpaqueValue<RootedThread, L>, OpaqueValue<RootedThread, R>>,
) -> Either<OpaqueValue<RootedThread, R>, OpaqueValue<RootedThread, L>> {
    match either {
        Either::Left(val) => Either::Right(val),
        Either::Right(val) => Either::Left(val),
    }
}

Now we can pass Either to our Rust function:

// Either is defined as:
// type Either l r = | Left l | Right r
let either: forall r . Either String r = Left "hello rust!"

// we can pass the generic Either to the Rust function without an issue
do _ =
    match flip either with
    | Left _ -> error "unreachable!"
    | Right val -> io.println ("Right is: " <> val)

// using an Int instead also works
let either: forall r . Either Int r = Left 42

match flip either with
| Left _ -> error "also unreachable!"
| Right 42 -> io.println "this is the right answer"
| Right _ -> error "wrong answer!"

Standard types and functions

The API documentation for the standard library can be found here. Some of the modules are only available if Gluon is compiled with the required features:

  • std.regex requires the regex feature (enabled by default)
  • std.random requires the rand feature (enabled by default)
  • All std.json.* modules require the serialization feature

TODO

Prelude

When compiling an expression, the compiler automatically inserts a small prelude before the expression itself, which gives automatic access to basic operators such as +, -, etc as well as types such as Option and Result.

Threads and channels

Gluon has support for cooperative threading and communication between them through the Thread and Sender/Receiver types.

TODO