MoonBit Task Checklist
- Locate the enclosing
moon.modand relevantmoon.pkgfiles before editing. - Discover APIs with
moon ide doc; useoutline,peek-def,find-references,hover, andrenamefor semantic navigation and refactoring. - Use
.mbtxexclusively for agent-authored automation; keep automation logic out of shell scripts and other scripting languages. - Keep changes within the correct package. Files do not define namespaces; separate top-level items with
///|. - Add regression tests for bug fixes. Add black-box tests and docstring examples for new public APIs.
- Validate with
moon checkand the narrowest relevantmoon test; usemoon test --updateonly for intended snapshot changes. Usemoon explain --diagnosticto list warnings and--warn-listto enable selected warnings (for example,+unnecessary_annotation). - Run
moon fmtandmoon infobefore handoff. Review generated.mbtichanges, especially when the public API should remain stable.
MoonBit Project Layouts
MoonBit uses the .mbt extension for source code files and interface files with the .mbti extension. At
the top-level of a MoonBit project there is a moon.mod file specifying
the metadata of the project. The project may contain multiple packages, each
with its own moon.pkg. Subdirectories may also contain moon.mod
files indicating that a different set of dependencies can be used for that subdir.
Legacy projects may still contain moon.mod.json; treat it as the old module
metadata format and migrate/update guidance to moon.mod instead of creating
new moon.mod.json files.
Example layout
my_module
├── moon.mod # Module metadata; source option can specify the source directory
├── moon.pkg # Package metadata (each directory is a package like Golang)
├── README.mbt.md # Markdown with tested code blocks (`test "..." { ... }`)
├── README.md -> README.mbt.md
├── cmd # Command line directory
│ └── main
│ ├── main.mbt
│ └── moon.pkg # executable package with `options("is-main": true)`
├── liba/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba`
│ └── libb/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba/libb`
├── user_pkg.mbt # Root packages, referenced by other packages as `@username/my_module`
├── user_pkg_wbtest.mbt # White-box tests (only needed for testing internal private members, similar to Golang's package mypackage)
└── user_pkg_test.mbt # Black-box tests
└── ... # More package files, symbols visible to current package (like Golang)
Module: characterized by a
moon.modfile in the project root directory. A MoonBit module is like a Go module; it is a collection of packages in subdirectories, usually corresponding to a repository or project. Module boundaries matter for dependency management and import paths.Package: characterized by a
moon.pkgfile in each directory. All subcommands ofmoonwill still be executed in the directory of the module (wheremoon.modis located), not the current package. A MoonBit package is the actual compilation unit (like a Go package). All source files in the same package are concatenated into one unit and thereby share all definitions throughout that package. Thenamein themoon.modfile combined with the relative path to the package source directory defines the package name, not the file name. Imports refer to module + package paths, NEVER to file names.Files: A
.mbtfile is just a chunk of source code inside a package. File names do NOT create modules, packages, or namespaces. You may freely split/merge/move declarations between files in the same package. Any declaration in a package can reference any other declaration in that package, regardless of file.
Coding/layout rules you MUST follow:
Prefer many small, cohesive files over one large file.
- Group related types and functions into focused files (e.g. http_client.mbt, router.mbt).
- If a file is getting large or unfocused, create a new file and move related declarations into it.
You MAY freely move declarations between files inside the same package.
- Each block is separated by
///|. Moving a function/struct/trait between files does not change semantics, as long as its name and pub-ness stay the same. The order of each block is irrelevant too. - It is safe to refactor by splitting or merging files inside a package.
- Each block is separated by
File names are purely organizational.
- Do NOT assume file names define modules, and do NOT use file names in type paths.
- Choose file names to describe a feature or responsibility, not to mirror type names rigidly.
When adding new code:
- Prefer adding it to an existing file that matches the feature.
- If no good file exists, create a new file under the same package with a descriptive name.
- Avoid creating giant "impl", “misc”, or “util” files.
Tests:
- Place tests in dedicated test files (e.g.
*_test.mbt) within the appropriate package. For a package (besides*_test.mbtfiles),*.mbt.mdfiles are also blackbox test files in addition to Markdown files. The code blocks (separated by triple backticks)mbt checkare treated as test cases and serve both purposes: documentation and tests. You may haveREADME.mbt.mdfiles withmbt checkcode examples. You can also symlinkREADME.mbt.mdtoREADME.mdto make it integrate better with GitHub. - It is fine — and encouraged — to have multiple small test files.
- Place tests in dedicated test files (e.g.
Interface files (
pkg.generated.mbti)pkg.generated.mbtifiles are compiler-generated summaries of each package's public API surface. They provide a formal, concise overview of all exported types, functions, and traits without implementation details. They are generated usingmoon infoand useful for code review. When you have a commit that does not change public APIs,pkg.generated.mbtifiles will remain unchanged, so it is recommended to putpkg.generated.mbtiin version control when you are done. Do not modifypkg.generated.mbtidirectly, including whitespace-only cleanup; regenerate it withmoon infoand review its diff as the public API signal.For IDE navigation and symbol lookup commands, see the dedicated
moon idesection below.
Common Pitfalls to Avoid
- Don't use uppercase for variables/functions - compilation error
- Don't forget
mutfor mutable record fields - immutable by default (note that Arrays typically do NOT needmutunless completely reassigning to the variable - simple push operations, for example, do not needmut) - Don't ignore error handling - either handle errors explicitly, or declare
raiseon the caller and let checked errors propagate - Don't use
returnunnecessarily - the last expression is the return value - Don't create methods without Type:: prefix - methods need explicit type prefix
- Don't forget to handle array bounds - use
get()for safe access - Don't forget @package prefix when calling functions from other packages
- Don't use ++ or -- (not supported) - use
i = i + 1ori += 1 - Don't add explicit
tryfor error propagation - inside araisefunction, call error-raising functions normally; usecatchto handle locally andtry!only when aborting is intended - Legacy syntax: Legacy code may use
function_name!(...)orfunction_name(...)?- these are deprecated; use normal calls for propagation. - Don't write an empty parameter list for
main- usefn main { ... }orfn main raise { ... }, notfn main() { ... }orfn main() raise ... { ... } - Don't write record-style enum or error constructor fields - labeled constructor fields use
label~ : Type, e.g.InvalidNumber(input~ : String), notInvalidNumber(input: String) - Prefer range
forloops over C-style -for i in 0..<(n-1) {...}andfor j in 0..=6 {...}are more idiomatic in MoonBit - Don't use
for { ... }for infinite loops - writefor ;; { ... }instead - Don't
derive(Show)for debugging - deriveDebugand usedebug_inspect()for test/diagnostic output (\{Repr(value)}for interpolation of composed values). Reserve a manualimpl Showfor specialized display formats (JSON, XML, domain text) - Don't call
@json.inspect()- use the preludejson_inspect(value, ...)without a package prefix - Async - MoonBit has no
awaitkeyword; do not add it. Async functions default to raising, so do not addraise; addnoraiseonly when the async body must not raise. Async functions and tests are characterized by those which call other async functions. To identify a function or test as async, simply add theasyncprefix (e.g.[pub] async fn ...,async test ...).
moon Essentials
Script Mode (.mbtx)
Use .mbtx exclusively for agent-authored automation. Keep loops,
conditionals, parsing, transformation, and process orchestration in MoonBit
instead of shell scripts or another scripting language. The outer shell should
only launch the script or run a direct, single-purpose command.
An .mbtx file is an optional import { ... } block followed by ordinary
MoonBit code, including a main function. Run it directly with:
moon run path/to/script.mbtx [args...]
Choose main by the effects the script actually uses:
fn main { ... }is synchronous and does not propagate errors.fn main raise { ... }is synchronous and may propagate checked errors.async fn main { ... }is for scripts that call async APIs. Import"moonbitlang/async"; do not addraiseorawait.
All three work on the default Wasm target. Do not mark a purely synchronous
script async; MoonBit reports unused_async.
When automation runs commands, import "moonbitlang/async/shell" and use
@shell.Cmd or @shell.Pipeline. They keep the executable and arguments
separate and never invoke a shell, so characters such as |, $(), and *
are passed literally. Use ordinary MoonBit control flow instead of &&, shell
loops, or command substitution.
Script mode defaults to Wasm. Supplying --wasm-policy policy.json enables
deny-by-default control over MoonBit host APIs; grant only the required
filesystem, environment, network, or process access. For process automation,
prefer a narrow process.allow rule with an exact program and argument prefix
over process.spawn: true. The policy authorizes a child process but does not
sandbox the child itself, so the host must also confine child processes when
they are not trusted.
Minimal script (hello.mbtx):
fn main {
println("Hello from MoonBit script mode")
}
Command-line automation (sum_args.mbtx):
import {
"moonbitlang/core/env",
"moonbitlang/core/string",
}
fn main raise {
let args = @env.args()
for arg in args[1:]; total = 0 {
continue total + @string.parse_int(arg)
} nobreak {
println(total)
}
}
Running moon run sum_args.mbtx 10 20 12 prints 42.
Package imports work directly in the script header (format_json.mbtx):
import {
"moonbitlang/core/json",
}
fn main raise {
let source =
#|{
#| "project": "moonbit",
#| "enabled": true
#|}
let value = @json.parse(source)
println(value.stringify(indent=2))
}
Async automation works on the default Wasm target. For example, run
moon run async_job.mbtx for:
import {
"moonbitlang/async",
}
async fn main {
@async.sleep(10)
println("async automation complete")
}
Essential Commands
moon new my_project- Create new projectmoon run cmd/main- Run main packagemoon run - < hello.mbtx- Run script code from stdin (useful for quick experiments)moon run -e "code snippet"- Run code from command line argument (good for one-liners)moon run -e 'fn main { println("Hello, MoonBit!") }'moon build- Build project (moon runandmoon buildboth support--target;moon buildalso supports--diagnostic-limit <N>)moon check- Type check without building, use it REGULARLY, it is fast (moon checkalso supports--targetand--diagnostic-limit <N>)moon info- Type check and generatembtifiles. Run it to see if any public interfaces changed. (moon infoalso supports--target.)moon check --target all- Type check for all backends Process structured diagnostics with a saved.mbtxscript rather than a shell pipeline or another scripting language. For example, save this asfilter_diagnostics.mbtx;@shell.Cmd::each_linerunsmoon checkdirectly, streams its JSON output, and returns its exit status:
import { "moonbitlang/async", "moonbitlang/async/shell", "moonbitlang/core/json", }
async fn main { let seen : Map[String, Unit] = Map([]) let exit_code = @shell.Cmd( "moon", ["check", "--target", "all", "--output-json"], ).each_line(line => { try @json.parse(line) catch { _ => () } noraise { { "level": "warning", "path": String(path), .. } => if !seen.contains(path) { seen[path] = () println(path) } _ => () } }) if exit_code != 0 { fail("moon check exited with code {exit_code}") } }
Run it with a policy that allows that command prefix:
```json
{
"process": {
"allow": [
{
"program": "moon",
"args_prefix": ["check", "--target", "all", "--output-json"]
}
]
}
}
moon run --wasm-policy moon-check-policy.json filter_diagnostics.mbtx
moon explain- Show built-in documentation for compiler diagnostics and language topics.moon explain --diagnosticlists warning mnemonics and IDs.moon explain --diagnostic 31explains warning 31 (unused_optional_argument).moon explain --diagnostic unused_optional_argumentexplains the same warning by mnemonic.moon explain --attributelists supported attributes such as#deprecated,#alias,#cfg,#coverage.skip, and#warnings.moon explain --attribute deprecatedexplains the#deprecatedattribute and its supported forms.
moon add package- Add dependencymoon remove package- Remove dependencymoon fmt- Format code - should be run periodically - note that the files may be rewritten Note you can also usemoon -C dir checkto run commands in a specific directory.
Profiling Hot Paths (moon run --profile)
moon run --profile --target native --release cmd/<main> runs a native release build under a sampling profiler and prints ranked self-time and inclusive-time tables plus a "runtime leaf costs attributed to MoonBit callers" section (which maps allocation, reference-counting, and string-equality costs back to your functions), alongside a profile.json and a .trace you can open in Instruments. On macOS it needs Xcode's xcrun xctrace, so install the full Xcode (not just the command-line tools) first. A single parse or compute is far too short to sample meaningfully, so point the profiled main at a loop that exercises the hot path a few hundred times over a representative fixture; this loop harness is throwaway and should never be committed.
Read self-time for which function burns cycles and inclusive-time for which call subtree dominates, then work a tight loop: profile, fix the top item, re-profile. Always re-baseline before trusting a delta — sampled timings drift with machine load, so build and benchmark the branch and main back-to-back (interleaved) rather than comparing against a number from an earlier session.
Test Commands
moon test- Run all tests (moon testalso supports--target)moon test --update- Update snapshotsmoon test -v- Verbose output with test namesmoon test [dirname|filename]- Test specific directory or filemoon coverage analyze- Analyze coveragemoon test [dirname|filename] --filter 'glob'- Run tests matching filtermoon test float/float_test.mbt --filter "Float::*" moon test float -F "Float::*" // shortcut syntax
README.mbt.md Generation Guide
- Output
README.mbt.mdin the package directory.*.mbt.mdfile and docstring contents treatsmbt checkspecially.mbt checkblock will be included directly as code and also run bymoon checkandmoon test. If you don't want the code snippets to be checked, explicitmbt nocheckis preferred. If you are only referencing types from the package, you should usembt nocheckwhich will only be syntax highlighted. SymlinkREADME.mbt.mdtoREADME.mdto adapt to systems that expectREADME.md.
Testing Guide
Use snapshot tests as it is easy to update when behavior changes.
Snapshot Tests: write
inspect(value)/debug_inspect(value)/json_inspect(value), then runmoon test --update(ormoon test -u) to fill incontent=.- Use
inspect()for values that implementShow(primitives, or types with a manualimpl Show). - Use
debug_inspect()for any type that derivesDebug— the default for your own data types. - Use
json_inspect()for complex nested structures (uses theToJsontrait, produces more readable output). - It is encouraged to inspect the whole return value of a function if it is not huge; this keeps the test simple. Derive
Debugand/orToJson(orimpl Show) onYourTypeaccordingly.
- Use
Update workflow: After changing code that affects output, run
moon test --updateto regenerate snapshots, then review the diffs in your test files (thecontent=parameter will be updated automatically).Validation order: Follow the
MoonBit Task Checklist.Black-box by default: Call only public APIs via
@package.fn. Use white-box tests only when private members matter.Grouping: Combine related checks in one
test "..." { ... }block for speed and clarity.Panics: Name tests with prefix
test "panic ..." {...}; if the call returns a value, wrap it withignore(...)to silence warnings.Errors: For expected success, call error-raising functions directly. If a call unexpectedly raises, the test fails with the actual error. For expected failure, use
try ... catch ... noraise, inspect the error incatch, and fail explicitly innoraise. Default expected-failure shape:try f() catch { err => inspect(err) } noraise { _ => fail("expected to fail") }.
Docstring tests
Public APIs are encouraged to have docstring tests.
///|
/// Return the sum of an `Array`.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(sum_array([1, 2, 3, 4, 5, 6]), content="21")
/// }
/// ```
pub fn sum_array(xs : Array[Int]) -> Int {
xs.fold(init=0, (a, b) => a + b)
}
The MoonBit code in a docstring will be type checked and tested automatically
(using moon test --update). In docstrings, mbt check should only contain test or async test.
Spec-driven Development
- The spec can be written in a readonly
spec.mbtfile (name is conventional, not mandatory) with stub code marked as declarations:
///|
declare pub type Yaml
///|
declare pub fn Yaml::to_string(y : Yaml) -> String raise
///|
declare pub impl Eq for Yaml
///|
declare pub fn parse_yaml(s : String) -> Yaml raise
Add
spec_easy_test.mbt,spec_difficult_test.mbt, etc. to test the spec functions; everything will be type-checked(moon check).The AI or users can implement the
declarefunctions in different files thanks to our package organization.Run
moon testto check everything is correct.declareis supported for functions, methods, and types.The
pub type Yamlline is an intentionally opaque placeholder; the implementer chooses its representation.Note the spec file can also contain normal code, not just declarations.
moon ide [doc|peek-def|outline|find-references|hover|rename|analyze] for code navigation and refactoring
For project-local symbols and navigation, use:
moon ide doc <query>to discover available APIs, functions, types, and methods in MoonBit. Always prefermoon ide docover other approaches when exploring what APIs are available, it is more powerful and accurate thangrep_searchor any regex-based searching tools.moon ide outline .to scan a package,moon ide find-references <symbol>to locate usages, andmoon ide peek-deffor inline definition context and to locate toplevel symbols.moon ide hover sym --loc filename:line:colto get type information at a specific location.moon ide rename <symbol> <new_name> [--loc filename:line:col]to rename a symbol project-wide. Prefer--locwhen symbol names are ambiguous.moon ide analyze [path]to inspect public API usage of a package or module when planning safe refactors. These tools save tokens and are more precise than grepping (grepdisplays results in both definitions and call sites including comments too).
moon ide doc for API Discovery
moon ide doc uses a specialized query syntax designed for symbol lookup:
Empty query:
moon ide doc ''- In a module: shows all available packages in current module, including dependencies and moonbitlang/core
- In a package: shows all symbols in current package
- Outside package: shows all available packages
Function/value lookup:
moon ide doc "[@pkg.]value_or_function_name"Type lookup:
moon ide doc "[@pkg.]Type_name"(builtin type does not need package prefix)Method/field lookup:
moon ide doc "[@pkg.]Type_name::method_or_field_name"Package exploration:
moon ide doc "@pkg"- Show package
pkgand list all its exported symbols - Example:
moon ide doc "@json"- explore entire@jsonpackage - Example:
moon ide doc "@encoding/utf8"- explore nested package
- Show package
Multiple queries:
moon ide doc "query1" "query2" ...- Run multiple queries in one invocation and combine results
- Example:
moon ide doc "String" "Array" "@json"to explore multiple types and a package at once
Globbing: Use
*wildcard for partial matches, e.g.moon ide doc "String::*rev*"to find all String methods with "rev" in their name
moon ide doc Examples
# search for String methods in standard library:
$ moon ide doc "String"
type String
pub fn String::add(String, String) -> String
# ... more methods omitted ...
$ moon ide doc "@buffer" # list all symbols in package buffer:
moonbitlang/core/buffer
fn from_array(ArrayView[Byte]) -> Buffer
# ... omitted ...
$ moon ide doc "@buffer.new" # list the specific function in a package:
package "moonbitlang/core/buffer"
pub fn new(size_hint? : Int) -> Buffer
Creates ... omitted ...
$ moon ide doc "String::*rev*" # globbing
package "moonbitlang/core/string"
pub fn String::rev(String) -> String
Returns ... omitted ...
# ... more
pub fn String::rev_find(String, StringView) -> Int?
Returns ... omitted ...
Best practice: Treat this section as command reference; validation is defined in the MoonBit Task Checklist.
moon ide rename sym new_name [--loc filename:line:col] example
When the user asks: "Can you rename the function compute_sum to calculate_sum?"
$ moon ide rename compute_sum calculate_sum --loc math_utils.mbt:2
*** Begin Patch
*** Update File: cmd/main/main.mbt
@@
///|
fn main {
- println(@math_utils.compute_sum(1, 2))
+ println(@math_utils.calculate_sum(1, 2))
}
*** Update File: math_utils.mbt
@@
///|
-pub fn compute_sum(a: Int, b: Int) -> Int {
+pub fn calculate_sum(a: Int, b: Int) -> Int {
a + b
}
*** Update File: math_utils_test.mbt
@@
///|
test {
- inspect(@math_utils.compute_sum(1, 2))
+ inspect(@math_utils.calculate_sum(1, 2))
}
*** End Patch
moon ide hover sym --loc filename:line:col example
When the user asks: "What is the signature and docstring of filter? at line 14 of hover.mbt"
$ moon ide hover filter --loc hover.mbt:14
test {
let a: Array[Int] = [1]
inspect(a.filter((x) => {x > 1}))
^^^^^^
```moonbit
fn[T] Array::filter(self : Array[T], f : (T) -> Bool raise?) -> Array[T] raise?
```
---
Creates a new array containing all elements from the input array that satisfy
... omitted ...
}
moon ide peek-def sym [--loc filename:line:col] example
When the user asks: "Can you check if Parser::read_u32_leb128 is implemented correctly?"
you can run moon ide peek-def Parser::read_u32_leb128 to get the definition context
(this is better than grep since it searches the whole project by semantics):
L45:|///|
L46:|fn Parser::read_u32_leb128(self : Parser) -> UInt raise ParseError {
L47:| ...
...:| }
Now if you want to see the definition of the Parser struct, you can run:
$ moon ide peek-def Parser --loc src/parse.mbt:46:4
Definition found at file src/parse.mbt
| ///|
2 | priv struct Parser {
| ^^^^^^
| bytes : Bytes
| mut pos : Int
| }
|
For the --loc argument, the line number must be precise; the column can be approximate since
the positional argument Parser helps locate the position.
If the "sym" is a toplevel symbol, the location can be omitted:
$ moon ide peek-def String::rev
Found 1 symbols matching 'String::rev':
`pub fn String::rev` in package moonbitlang/core/builtin at /Users/usrname/.moon/lib/core/builtin/string_methods.mbt:1039-1044
1039 | ///|
| /// Returns a new string with the characters in reverse order. It respects
| /// Unicode characters and surrogate pairs but not grapheme clusters.
| pub fn String::rev(self : String) -> String {
| self[:].rev()
| }
moon ide outline [dir|file] and moon ide find-references <sym> for Package Symbols
Use moon ide outline to scan a package or file for top-level symbols and locate usages without grepping.
moon ide outline diroutlines the current package directory (per-file headers)moon ide outline parser.mbtoutlines a single file This is useful when you need a quick inventory of a package, or to find the right file beforepeek-def.moon ide find-references TranslationUnitfinds all references to a symbol in the current module
$ moon ide outline .
spec.mbt:
L003 | pub(all) enum CStandard {
...
L013 | pub(all) struct Position {
...
$ moon ide find-references TranslationUnit
Package Management
Adding Dependencies
moon add moonbitlang/x # Add latest version
moon add moonbitlang/x@0.4.6 # Add specific version
Updating Dependencies
moon update # Update package index
Browsing Third-Party Source (moon fetch)
moon fetch <author>/<module>[@<version>] downloads a package's source into .repos/<author>/<module>/<version>/ for offline reading (examples, internals, generated .mbti). It does NOT add the package to moon.mod — use moon add for that. Add .repos/ to .gitignore.
moon fetch moonbitlang/async@0.18.1 # browse source/examples without taking a dependency
Typical Module configurations (moon.mod)
name = "username/hello"
version = "0.1.0"
readme = "README.mbt.md"
repository = ""
license = "Apache-2.0"
keywords = []
description = "..."
import {
"moonbitlang/x@0.4.6",
}
options(
// source: "src", // Optional; default is "."
"preferred-target": "native",
)
Use moon add moonbitlang/x@0.4.6 and moon remove moonbitlang/x to manage the import block instead of editing dependency versions by hand.
Typical Package configuration (moon.pkg)
moon.pkg for simplicity
import {
"username/hello/liba",
"moonbitlang/x/encoding" @libb,
}
import {
"username/hello/test_helpers",
} for "test"
import {
"username/hello/internal_test_helpers",
} for "wbtest"
options(
"is-main": true,
)
Use supported_targets = "native" or another target-set expression at top level when the whole package only supports selected backends.
supported_targets = "native"
options(
"is-main": true,
)
Packages are per directory and packages without a moon.pkg file are not recognized.
Package Importing (used in moon.pkg)
- Import format:
"module_name/package_path" - Usage:
@alias.function()to call imported functions - Default alias: Last part of path (e.g.,
libaforusername/hello/liba) - Package reference: Use
@packagenamein test files to reference the tested package
Package Alias Rules:
- Import
"username/hello/liba"→ use@liba.function()(default alias is the last path segment) - Import with custom alias
import { "moonbitlang/x/encoding" @enc}→ use@enc.function()(Note that this is unnecessary when the last path segment is identical to the alias name.) - In
_test.mbtor_wbtest.mbtfiles, the package being tested is auto-imported
Example:
///|
/// In main.mbt after importing "username/hello/liba" in `moon.pkg`
fn main {
println(@liba.hello()) // Calls hello() from liba package
}
Using the Standard Library (moonbitlang/core)
The moonbitlang/core module is always available without adding it to moon.mod dependencies. Ordinary core packages still need explicit moon.pkg imports for package aliases such as @utf8, @json, or @strconv; add imports like "moonbitlang/core/encoding/utf8" when the compiler reports a missing or implicit core package.
Creating Packages
To add a new package fib under .:
Create directory:
./fib/Add
./fib/moon.pkgAdd
.mbtfiles with your codeImport in dependent packages:
import { "username/hello/fib", }
For more advanced topics like conditional compilation, link configuration, warning control, and pre-build commands, see references/advanced-moonbit-build.md.
Async IO
Asynchronous programming uses compiler support plus the moonbitlang/async
runtime. async fn main works on the default Wasm target as well as native when
the runtime is imported. Individual host I/O APIs may still be target-specific;
use moon ide doc "@async" and validate on the intended target.
User-facing subpackages include @async (tasks, timers, cancellation),
@async/aqueue, @async/fs, @async/shell (shell-free process orchestration),
@async/stdio, and @async/websocket.
Each must be imported separately in moon.pkg or an .mbtx import block.
- Add the dependency with
moon add moonbitlang/async. - In the executable's
moon.pkg, setis-mainand import what you need:import { "moonbitlang/async", "moonbitlang/async/stdio", } options( "is-main": true, ) - Define
async fn main(notfn main). Spawn concurrent tasks viawith_task_groupfor structured concurrency:///| async fn main { @async.with_task_group(group => { group.spawn_bg(() => { @async.sleep(50) @stdio.stdout.write("A\n") }) group.spawn_bg(() => { @async.sleep(20) @stdio.stdout.write("B\n") }) }) }
- Async functions have a raising effect by default. Write
async fn main { ... }orasync fn f(...) { ... }, notasync fn main raise { ... }. - Use
async fn f(...) noraise { ... }only when the async body must not raise. Anoraiseasync function cannot call fallible APIs unless it handles their errors locally.
Structured-concurrency contract for with_task_group:
- When
with_task_groupreturns, every task spawned in the group is guaranteed to have terminated — no orphan tasks, no resource leaks. - If any spawned task fails (and was spawned without
allow_failure=true), the whole group fails: every other task in the group is cancelled, and the error propagates out ofwith_task_group. - Cancelled tasks are not considered failures; they raise a cancellation error but don't trigger peer cancellation.
Closure syntax for spawn_bg / spawn:
- ✅
() => { ... }— idiomatic; async-ness is inferred from context. - ✅
async fn() { ... }— explicit annotation; equivalent to the arrow form. - ⚠️
fn() { ... }— triggersWarning [0027] deprecated_syntax: "thisfnis asynchronous but not annotated withasync". Don't use. - ❌
async () => ...,fn() async { ... },fn(args) async { ... }— all parse errors.asynconly goes beforefn, never before an arrow lambda or after a parameter list.
Async tests
Use async test for tests that call async functions. The package containing the test must import moonbitlang/async for the test mode; import any async subpackages used by the test in the same for "test" block.
import {
"moonbitlang/async",
"moonbitlang/async/stdio",
} for "test"
///|
async test "sleep completes" {
@async.sleep(1)
inspect("done", content="done")
}
- There is no
awaitkeyword (similar to functions that raise errors). Inside anasync test, call async functions normally. async testalso has the async raising effect by default; do not addraise.- Async tests run in parallel by default. Avoid shared ports, files, environment variables, and global mutable state unless each test isolates its resources.
- Run
moon teston the intended target; add--target nativeonly when the APIs under test require native execution. Usemoon test -vwhen checking test names or async scheduling behavior. - In
README.mbt.mdand docstrings,mbt checkblocks may containasync testblocks; make sure the package importsmoonbitlang/asyncfor the relevant test mode.
MoonBit Language Tour
Core facts
- Expression‑oriented:
if,match, loops return values; the last expression is the return value. - References by default: Arrays/Maps/structs mutate via reference; use
Ref[T]for primitive mutability. - Blocks: Separate top‑level items with
///|. Generate code block‑by‑block. If a blank line is desired within a block (enclosed by curly braces), add a comment line after the blank line (with or without comment text). - Visibility:
fnis private by default;pubexposes read/construct as allowed;pub(all)allows external construction. - Naming convention: lower_snake for values/functions; UpperCamel for types/enums; enum variants start UpperCamel.
- Packages: No
importin code files; call via@alias.fn. Configure imports inmoon.pkg. - Placeholders:
...is a valid placeholder in MoonBit code for incomplete implementations. - Global values: immutable by default and generally require type annotations.
- Garbage collection: MoonBit has a GC, there is no lifetime annotation, there's no ownership system.
Unlike Rust, like F#,
let mutis only needed when you want to reassign a variable, not for mutating fields of a struct or elements of an array/map.
MoonBit Error Handling (Checked Errors)
MoonBit uses checked error-throwing functions, not unchecked exceptions. All errors are a subtype of Error, and you can declare your own error types using suberror.
Checked errors are tracked in function signatures, not marked at every call site. A function that may raise declares raise or raise SomeError. If the caller only wants to pass that error upward, the caller also declares a compatible raise and calls the raising function normally.
- Plain call inside a
raisefunction: propagate automatically. fn main raise { ... }is valid for synchronous command-line probes and small examples that should propagate errors. For async entry points, useasync fn main { ... }; async functions can raise by default.- In
suberrorconstructors, labeled payloads uselabel~ : Type; call and pattern-match them withlabel=value. expr catch { ... }ortry { ... } catch { ... }: handle explicitly.try! expr: abort if an error is raised.
Do not add Swift-style try for propagation. Do not use legacy function_name!(...) or function_name(...)? syntax for new code.
///|
/// Declare error types with 'suberror'
suberror ValueError {
ValueError(String)
}
///|
/// Tuple struct to hold position info
struct Position(Int, Int) derive(ToJson, Debug, Eq)
///|
/// ParseError is subtype of Error
pub(all) suberror ParseError {
InvalidChar(pos~ : Position, Char) // pos is labeled
InvalidEof(pos~ : Position)
InvalidNumber(pos~ : Position, String)
InvalidIdentEscape(pos~ : Position)
} derive(Eq, ToJson, Debug)
///|
/// Functions declare what they can throw
fn parse_int(s : String, position~ : Position) -> Int raise ParseError {
// 'raise' throws an error
if s is "" {
raise ParseError::InvalidEof(pos=position)
}
... // parsing logic
}
///|
/// Declare a specific error type when callers should handle it precisely
fn div(x : Int, y : Int) -> Int raise ValueError {
if y is 0 {
raise ValueError::ValueError("Division by zero")
}
x / y
}
///|
test "expected success calls directly" {
inspect(div(6, 3), content="2")
}
///|
test "expected failure handles the raised error" {
try div(1, 0) catch {
ValueError::ValueError(message) => inspect(message, content="Division by zero")
} noraise {
_ => fail("expected to fail")
}
}
// Three ways to handle errors:
///|
/// Propagate automatically
fn use_parse(s : String, position~ : Position) -> Int raise ParseError {
// This plain call is the correct propagation syntax.
// `try! parse_int(...)` would abort instead of propagating.
let x = parse_int(s, position~) // label punning, equivalent to position=position
// Error auto-propagates by default.
// Unlike Swift, you do not need to mark `try` for functions that can raise
// errors; the compiler infers it automatically. This keeps error handling
// explicit but concise.
x * 2
}
///|
/// Use try! to abort if it raises, no raise in the signature
fn use_parse2(position~ : Position) -> Int {
let x = try! parse_int("123", position~) // label punning
x * 2
}
///|
/// Handle with try-catch
fn handle_parse(s : String, position~ : Position) -> Int {
parse_int(s, position~) catch {
ParseError::InvalidEof(pos=_) => {
println("Parse failed: InvalidEof")
-1 // Default value
}
_ => 2
}
}
Important: When calling a function that can raise errors, if you only want to
propagate the error, you do not need any marker; the compiler infers it.
Async functions automatically can raise errors without explicitly stating this. Do not add raise to async functions for propagation; add noraise only when the async function must reject unhandled errors.
Integer, Char and overloaded literals
MoonBit supports Byte, Int16, Int, UInt16, UInt, Int64, UInt64, etc.
When the type is known, the literal can be overloaded:
///|
test "integer and char literal overloading disambiguation via type in the current context" {
let (int, uint, uint16, int64, byte) : (Int, UInt, UInt16, Int64, Byte) = (
1, 1, 1, 1, 1,
)
// The literal `1` is overloaded based on the expected type in the current context.
// compile time error if the literal cannot be represented in the target type,
// e.g. let a7 : Byte = 256 // ❌ won't compile, 256 exceeds Byte max value 255
assert_eq(int, uint16.to_int())
let (a1, a2, a3) : (Int, Char, UInt16) = ('b', 'b', 'b')
// char literal overloading, `a1` will be the unicode value of 'b',
// compile time error when the literal cannot be represented in the target type
// e.g, let a6 : UInt16 = '𐍈' // ❌
…(truncated)