MoonBit's Build System Tutorial#
Moon is the build system for the MoonBit language, currently based on the n2 project. Moon supports parallel and incremental builds. Additionally, moon also supports managing and building third-party packages on mooncakes.io
Prerequisites#
Before you begin with this tutorial, make sure you have installed the following:
MoonBit CLI Tools: Download it from the https://www.moonbitlang.com/download/. This command line tool is needed for creating and managing MoonBit projects.
Use
moon helpto view the usage instructions.$ moon help ...
MoonBit Language plugin in Visual Studio Code: You can install it from the VS Code marketplace. This plugin provides a rich development environment for MoonBit, including functionalities like syntax highlighting, code completion, and more.
Once you have these prerequisites fulfilled, let's start by creating a new MoonBit module.
Creating a New Module#
To create a new module, enter the moon new <path> command in the terminal, where the path is the directory that you'd like to put the project, and you will see the module being created. By using all the default values, you can create a new module named username/my_project in the my_project directory.
$ moon new my_project
Initialized empty Git repository in my_project/.git/
Created username/my_project at my_project
You may also specify the username and module name by using the --user option and --name option respectively.
If you have logged-in, the username will default to your username.
Understanding the Module Directory Structure#
After creating the new module, your directory structure should resemble the following:
my_project
├── Agents.md
├── cmd
│ └── main
│ ├── main.mbt
│ └── moon.pkg
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── my_project_test.mbt
├── my_project.mbt
├── README.mbt.md
└── README.md -> README.mbt.md
Note
On Windows system, you need administrator privilege or the developer mode enabled to create the symbolic link.
Here's a brief explanation of the directory structure:
moon.mod.jsonis used to identify a directory as a MoonBit module. It contains the module's metadata, such as the module name, version, etc.{ "name": "username/my_project", "version": "0.1.0", "readme": "README.md", "repository": "", "license": "Apache-2.0", "keywords": [], "description": "" }
.andcmd/maindirectories: These are the packages within the module. Each package can contain multiple.mbtfiles, which are the source code files for the MoonBit language. However, regardless of how many.mbtfiles a package has, they all share a commonmoon.pkgfile. Older projects may still use the legacymoon.pkg.jsonformat.*_test.mbtare separate test files in the package, these files are for blackbox tests, so private members of the same package cannot be accessed directly.moon.pkgis the package descriptor. It defines the properties of the package, such as whether it is the main package and the packages it imports.cmd/main/moon.pkg:import { "username/my_project" @lib, } options( "is-main": true, )
Here,
"is-main": truedeclares that the package contains an entry for themoon runcommand.moon.pkg:This file may be empty. Its purpose is simply to inform the build system that this folder is a package.
README.mbt.mdis the README file. In this file,mbt checkcode blocks are checked and run bymoon checkandmoon test.
Working with Packages#
Our username/my_project module contains two packages: username/my_project and username/my_project/cmd/main.
The username/my_project package contains my_project.mbt and my_project_test.mbt files:
///|
pub fn fib(n : Int) -> Int64 {
for i = 0, a = 0L, b = 1L; i < n; i = i + 1, a = b, b = a + b {
} nobreak {
b
}
}
///|
test "fib" {
let array = [1, 2, 3, 4, 5].map(fib(_))
// `inspect` is used to check the output of the function
// Just write `inspect(value)` and execute `moon test --update`
// to update the expected output, and verify them afterwards
inspect(array, content="[1, 2, 3, 5, 8]")
}
Note
The generated file name will depend on the package name.
The username/my_project/cmd/main package contains a main.mbt file:
///|
fn main {
println(@lib.fib(10))
}
To execute the program, specify the file system's path to the username/my_project/cmd/main package in the moon run command:
$ moon run cmd/main
89
You can test using the moon test command:
$ moon test
Total tests: 1, passed: 1, failed: 0.
Choosing a Target#
Moon has three different target-related knobs, and they serve different jobs:
--targeton the command line chooses which backend the current command usespreferred-targetinmoon.mod.jsonchooses the default backend formoonand the language serversupported-targetsdeclares which backends a module or package is intended to support
For example, a native-first CLI project may set:
{
"preferred-target": "native",
"supported-targets": "native"
}
supported-targets uses target-set syntax such as js, +js+wasm-gc, or +all-js.
If only some files are backend-specific, keep the module or package metadata broad and use
targets in moon.pkg or legacy moon.pkg.json to select files
per backend.
Package Importing#
In the MoonBit build system, dependencies are declared at the package level.
To import the username/my_project package in username/my_project/cmd/main, you need to specify it in cmd/main/moon.pkg:
import {
"username/my_project" @lib,
}
options(
"is-main": true,
)
Here, "username/my_project" specifies importing the root package and having an alias of lib, so you can use @lib.fib(10) in cmd/main/main.mbt.
Creating and Using a New Package#
First, create a new directory named fib in the module root:
mkdir fib
Now, you can create new files under fib:
pub fn fib_slow(n : Int) -> Int {
match n {
0 => 0
1 => 1
_ => fib_slow(n - 1) + fib_slow(n - 2)
}
}
pub fn fib_fast(num : Int) -> Int {
fn aux(n, acc1, acc2) {
match n {
0 => acc1
1 => acc2
_ => aux(n - 1, acc2, acc1 + acc2)
}
}
aux(num, 0, 1)
}
// This package does not need extra options yet.
After creating these files, your directory structure should look like this:
.
├── Agents.md
├── cmd
│ └── main
│ ├── main.mbt
│ └── moon.pkg
├── fib
│ ├── fast.mbt
│ ├── moon.pkg
│ └── slow.mbt
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── my_project_test.mbt
├── my_project.mbt
├── README.mbt.md
└── README.md -> README.mbt.md
In the cmd/main/moon.pkg file, import the username/my_project/fib package and customize its alias to my_awesome_fibonacci:
import {
"username/my_project/fib" @my_awesome_fibonacci,
}
options(
"is-main": true,
)
This imports the fib package. After doing this, you can use the fib package in cmd/main/main.mbt. Replace the file content of cmd/main/main.mbt to:
fn main {
let a = @my_awesome_fibonacci.fib_slow(10)
let b = @my_awesome_fibonacci.fib_fast(11)
println("fib(10) = \{a}, fib(11) = \{b}")
}
To execute your program, specify the path to the main package:
$ moon run cmd/main
fib(10) = 55, fib(11) = 89
Adding Tests#
Let's add some tests to verify our fib implementation. Add the following content in fib/fib_test.mbt:
test {
inspect(@fib.fib_slow(0))
inspect(@fib.fib_slow(1))
inspect(@fib.fib_slow(2))
}
This code tests the first three terms of the Fibonacci sequence. test { ... } defines an inline test block. The code inside an inline test block is executed in test mode.
Inline test blocks are discarded in non-test compilation modes (moon build and moon run), so they won't cause the generated code size to bloat.
Here we are using the snapshot test. Execute moon test --update, and the file should be changed to:
test {
inspect(@fib.fib_slow(0), content="0")
inspect(@fib.fib_slow(1), content="1")
inspect(@fib.fib_slow(2), content="1")
}
Notice that the test code uses @fib to refer to the fib package. The build system automatically creates a new package for blackbox tests by using the files that end with _test.mbt.
Finally, reuse the moon test command, which scans the entire project, identifies, and runs all the tests.
If everything is normal, you will see:
$ moon test
Total tests: 2, passed: 2, failed: 0.