Introduction

Note: Mun & this book are currently under active development, any and all content of this book is not final and may still change.

Mun is an embeddable scripting language designed for developer productivity.

  • Ahead of time compilation
    Mun is compiled ahead of time (AOT), as opposed to being interpreted or compiled just in time (JIT). By detecting errors in the code during AOT compilation, an entire class of runtime errors is eliminated. This allows developers to stay within the comfort of their IDE instead of having to switch between the IDE and target application to debug runtime errors.

  • Statically typed
    Mun resolves types at compilation time instead of at runtime, resulting in immediate feedback when writing code and opening the door for powerful refactoring tools.

  • First class hot-reloading
    Every aspect of Mun is designed with hot reloading in mind. Hot reloading is the process of changing code and resources of a live application, removing the need to start, stop and recompile an application whenever a function or value is changed.

  • Performance
    AOT compilation combined with static typing ensure that Mun is compiled to machine code that can be natively executed on any target platform. LLVM is used for compilation and optimization, guaranteeing the best possible performance. Hot reloading does introduce a slight runtime overhead, but it can be disabled for production builds to ensure the best possible runtime performance.

  • Cross compilation
    The Mun compiler is able to compile to all supported target platforms from any supported compiler platform.

  • Powerful IDE integration
    The Mun language and compiler framework are designed to support source code queries, allowing for powerful IDE integrations such as code completion and refactoring tools.

Case Studies

A collection of case studies that inspired the design choices made in Mun.

Abbey Games

Abbey Games uses Lua as its main gameplay programming language because of Lua's ability to hot reload code. This allows for rapid iteration of game code, enabling gameplay programmers and designers to quickly test and tweak systems and content. Lua is a dynamically typed, JIT compiled language. Although this has some definite advantages, it also introduces a lot of problems with bigger codebases.

Changes in Lua code can have large implications throughout the entire codebase and since we cannot oversee the entire codebase at all times runtime errors are bound to occur. Runtime errors are nasty beasts because they can pop up after a long period of time and after work on the offending piece of code has already finished. They are also often detected by someone different from the person who worked on the code. This causes great frustration and delay, let alone when the runtime error is detected by a user of the software.

Lua amplifies this issue due to its dynamic and flexible nature. It would be great if we could turn some of these runtime errors into compile time errors. That way programmers are notified of errors way before someone else runs into them. The risk of causing implicit runtime errors causes programmers to distrust their refactoring tools. This in turn reduces the likelihood of programmers refactoring their code.

Even though Lua offers immense flexibility, we noticed that certain opinionated patterns recur a lot and as such have become standard practice. Introducing these practices assists us in daily development a lot, but requires more code and complexity than desirable. Having syntactic sugar would greatly help reduce complexity in our code base, but would also introduce magic or custom keywords that are foreign to both new developers and IDE's.

Rapid iteration is key to prototyping game concepts and features. Proper IDE-integration of a scripting language gives a huge boost to productivity.

Getting Started

Let's start your Mun journey by creating installing the Mun CLI and creating a simple Mun library. We'll then show you how to make it hot reloadable by embedding it into an application.

Installation

First we need to install the Mun CLI (command-line interface), which acts as an all-in-one tool for Mun application development. Pre-built binaries are available for macOS, Linux, and Windows (64-bit only). Download and extract the binaries to a location of your preference.

Congratulations! You are now ready to write your first Mun code.

Hello, fibonacci?

Most programming languages start off with a "Hello, world!" example, but not Mun. Mun is designed around the concept of hot reloading. Our philosophy is to only add new language constructs when those can be hot reloaded. Since the first building blocks of Mun were native types and functions our divergent example has become fibonacci, hence "Hello, fibonacci?".

Creating a Project Directory

The Mun compiler is agnostic to the location of a project directory, as long as all source files are in the same place. Let's open a terminal to create our first project directory:

mkdir hello_fibonacci
cd hello_fibonacci

Writing and Running a Mun Library

Next, make a new source file and call it hello_fibonacci.mun. Mun files always end with the .mun extension. If your file name consists of multiple words, separate them using underscores.

Open up the new source file and enter the code in Listing 1-1.

Filename: hello_fibonacci.mun

fn fibonacci5():int {
    fibonacci(5)
}

fn fibonacci(n:int):int {
    if n <= 1 {
        n
    } else {
        fibonacci(n-1) + fibonacci(n-2)
    }
}

Listing 1-1: A function that calculates a fibonacci number

Save the file and go back to your terminal window. You are now ready to compile your first Mun library. Enter the following command to compile the file:

mun build hello_fibonacci.mun

Contrary to many other languages, Mun doesn't support standalone applications, instead it is shipped in the form of shared libraries. That's why Mun comes with a command-line interface (CLI) that can both compile and run Mun libraries. As each operating system has their own extension for shared libraries, you'll need to use slighty different commands on different operating systems.

On Linux, enter the following command run the Mun library:

mun start hello_fibonacci.so --entry fibonacci5

On macOS, enter the following commands to run the Mun library:

mun start hello_fibonacci.dylib --entry fibonacci5

On Windows, enter the following commands to run the Mun library:

mun start hello_fibonacci.dll --entry fibonacci5

The result of fibonacci5 (i.e. 5) should now appear in your terminal. Congratulations! You just successfully created and ran your first Mun library.

Hello, Hot Reloading!

Mun distinguishes itself from other languages by its inherent hot reloading capabilities. The following example illustrates how you can create a hot reloadable application by extending the Hello, fibonacci? example.

Filename: hello_fibonacci.mun

fn fibonacci5():int {
    fibonacci(5)
}

fn fibonacci(n:int):int {
    if n <= 1 {
        n
    } else {
        fibonacci(n-1) + fibonacci(n-2)
    }
}

Listing 1-2: A function that calculates a fibonacci number

Apart from running Mun libraries from the command-line interface, a common use case is embedding them in other programming languages.

Mun embedded in C++

Mun exposes a C API and complementary C++ bindings for the Mun Runtime. Listing 1-3 shows a C++ application that constructs a Mun Runtime for the hello_fibonacci library and continuously invokes the fibonacci5 function and outputs its result.

Filename: main.cc

#include <iostream>

#include "mun/runtime.h"

int main() {
    if (argc < 2) {
        return 1;
    }

    auto lib_path = argv[1];
    if (auto runtime = mun::make_runtime(lib_path)) {
        while (true) {
            auto result = mun::invoke_fn<int64_t>(*runtime, "fibonacci5").wait();
            std::cout << "fibonacci5() = " << result << std::endl;

            runtime->update();
        }
    }

    return 2;
}

Listing 1-3: Hello, Fibonacci? embedded in a C++ application

Mun embedded in Rust

As the Mun Runtime is written in Rust, it can be easily embedded in Rust applications by adding the mun_runtime crate as a dependency. Listing 1-4 illustrates a simple Rust application that builds a Mun Runtime and continuously invokes the fibonacci5 function and prints its output.

Filename: main.rs

use mun_runtime::{invoke_fn, RetryResultExt, RuntimeBuilder};
use std::env;

fn main() {
    let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");

    let mut runtime = RuntimeBuilder::new(lib_path)
        .spawn()
        .expect("Failed to spawn Runtime");

    loop {
        let result: i64 = invoke_fn!(runtime, "fibonacci5").wait();
        println!("fibonacci5() = {}", result);
        runtime.update();
    }
}

Listing 1-4: Hello, Fibonacci? embedded in a Rust application

Hot Reloading

The prior examples both update the runtime every loop cycle. In the background, this detects recompiled code and reloads the resulting shared libraries.

To ensure that the Mun compiler recompiles our code every time the hello_fibonacci.mun source file from Listing 1-2 changes, the --watch argument must be added:

mun build hello_fibonacci.mun --watch

When saved, changes in the source file will automatically take affect in the running example application. E.g. change the n value passed to the fibonacci function and the application will log the corresponding Fibonacci number.

Some changes, such as a type mismatch between the compiled application and the hot reloadable library, can lead to runtime errors. When these occur, the runtime will log the error and halt until an update to the source code arrives.

That's it! Now you are ready to start developing hot reloadable Mun libraries.

Basic Concepts

This section describes the basic concepts of the Mun programming language.

Values and types

Mun is a statically typed language, which helps to detect type-related errors at compile-time. A type error is an invalid operation on a given type, such as an integer divided by a string, trying to access a field that doesn't exist, or calling a function with the wrong number of arguments.

Some languages require a programmer to explicitly annotate syntactic constructs with type information:

int foo = 3 + 4;

However, often variable types can be inferred by their usage. Mun uses type inferencing to determine variable types at compile time. However, you are still forced to explicitly annotate variables in a few locations to ensure a contract between interdependent code.

fn bar(a:int):int {
    let foo = 3 + a
    foo
}

Here, the parameter a and the return type must be annotated because it solidifies the signature of the function. The type of foo can be inferred through its usage.

Basic types

Mun knows two basic numeric types: float and int. A float is a double-precision IEEE 64-bit floating point number and an int represents a 64-bit integral number.

In Mun an int can be explicitly cast to float, but the reverse is not true. Assigning a float to an int might cause loss of precision and can therefore not be cast implicitly.

A bool has two possible values: true and false.

Implicit & explicit returns

A block is a grouping of statements and expressions surrounded by curly braces. Function bodies are an example of blocks. In Mun, blocks evaluate to the last expression in them. Blocks can therefore also be used on the right hand side of a let statement.

fn foo():int {
    let bar = {
        let b = 3;
        b+3
    }

    // `bar` has a value 6
    bar+3
}

Besides implicit returns, explicit returns can also be used with return expressions. However, explicit return statements always return from the function, not from the block:

fn foo():int {
    let bar = {
        let b = 3;
        return b+3
    }

    // This code will never be executed
    return bar+3;
}

Shadowing

WIP

Control flow

Executing or repeating a block of code only under specific conditions are common constructs that allow developers to control the flow of execution. Mun provides if...else expressions and loops.

if expressions

An if expression allows you to branch your code depending on conditions.

fn main() {
    let number = 3;

    if number < 5 {
        number = 4;
    } else {
        number = 6;
    }
}

All if expressions start with the keyword if, followed by a condition. As opposed to many C-like languages, Mun omits parentheses around the condition. Only when the condition is true - in the example, whether the number variable is less than 5 - the consecutive code block (or arm) is executed.

Optionally, an else expression can be added that will be executed when the condition evaluates to false. You can also have multiple conditions by combining if and else in an else if expression. For example:

fn main() {
    let number = 6;
    if number > 10 {
        // The number if larger than 10
    } else if number > 8 {
        // The number is larger than 8 but smaller or equal to 10
    } else if number > 2 {
        // The number is larger than 2 but smaller or equal to 8
    } else {
        // The number is smaller than- or equal to 2.
    }
}

Using if in a let statement

The if expression can be used on the right side of a let statement just like a block:

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

Depending on the condition, the number variable will be bound to the value of the if block or the else block. This means that both the if and else arms need to evaluate to the same type. If the types are mismatched the compiler will report an error.