moonbit-agent-guide/SKILL.md
Guide for writing, refactoring, and testing MoonBit projects. Use when working in MoonBit modules or packages, organizing MoonBit files, using moon tooling (build/check/run/test/doc/ide etc.), or following MoonBit-specific layout, documentation, and testing conventions.
npx skillsauth add moonbitlang/moonbit-agent-guide moonbit-agent-guideInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
For fast, reliable task execution, follow this order:
Clarify goal and constraints
Locate module/package boundaries
moon.mod (module root) and relevant moon.pkg files (package boundaries and imports).Discover APIs before coding
moon ide doc queries to discover existing functions/types/methods before adding new code.moon ide outline, moon ide peek-def, and moon ide find-references for semantic navigation.Reliable refactoring
moon ide rename for semantic refactoring. If multiple symbols share a name, add --loc filename:line:col.#alias(old_api, deprecated).Edit minimally and package-locally
///| top-level delimiters, and split code into cohesive files.Validate in a tight loop
moon check after edits, adding --warn-list +unnecessary_annotation to enable warning 73 for redundant annotations and over-qualified constructors (--warn-list +73 is equivalent).moon test [dirname|filename] --filter 'glob' and use moon test --update for snapshot changes.Finalize before handoff
moon fmt.moon info to verify whether public APIs changed (pkg.generated.mbti diff).Use the smallest playbook that matches the request.
moon ide outline, moon ide peek-def, moon ide find-references.moon checkmoon test [dirname|filename] --filter 'glob' (or closest targeted test scope)moon fmtmoon info (confirm pkg.generated.mbti unchanged)moon ide renamemoon ide find-referencesmoon ide peek-defmoon ide rename <symbol> <new_name> --loc filename:line:col.moon checkmoon test [dirname|filename]moon fmtmoon info (API should remain unchanged unless requested)moon ide doc before introducing new names.///| delimiters.moon checkmoon test [dirname|filename] (use --update for snapshots when needed)moon fmtmoon info (review and keep intended pkg.generated.mbti changes)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.
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.mod file 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.pkg file in each directory.
All subcommands of moon will
still be executed in the directory of the module (where moon.mod is
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.
The name in the moon.mod file 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 .mbt file 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.
Prefer many small, cohesive files over one large file.
You MAY freely move declarations between files inside the same package.
///|. 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.File names are purely organizational.
When adding new code:
Tests:
*_test.mbt) within the appropriate package.
For a package (besides *_test.mbtfiles), *.mbt.md files are also blackbox test files in addition to Markdown files.
The code blocks (separated by triple backticks) mbt check are treated as test cases and serve both purposes: documentation and tests.
You may have README.mbt.md files with mbt check code examples. You can also symlink README.mbt.md to README.md
to make it integrate better with GitHub.Interface files (pkg.generated.mbti)
pkg.generated.mbti files 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 using moon info and useful for code review. When you have a commit that does not change public APIs, pkg.generated.mbti files will remain unchanged, so it is recommended to put pkg.generated.mbti in version control when you are done.
For IDE navigation and symbol lookup commands, see the dedicated moon ide section below.
mut for mutable record fields - immutable by default (note that Arrays typically do NOT need mut unless completely reassigning to the variable - simple push operations, for example, do not need mut)raise on the caller and let checked errors propagatereturn unnecessarily - the last expression is the return valueget() for safe accessi = i + 1 or i += 1try for error propagation - inside a raise function, call error-raising functions normally; use catch to handle locally and try! only when aborting is intendedfunction_name!(...) or function_name(...)? - these are deprecated; use normal calls for propagation.main - use fn main { ... } or fn main raise { ... }, not fn main() { ... } or fn main() raise ... { ... }label~ : Type, e.g. InvalidNumber(input~ : String), not InvalidNumber(input: String)for loops over C-style - for i in 0..<(n-1) {...} and for j in 0..=6 {...} are more idiomatic in MoonBitfor { ... } for infinite loops - write for ;; { ... } insteadderive(Show) for debugging - derive Debug and use debug_inspect() for test/diagnostic output (\{to_repr(value)} for interpolation of composed values). Reserve a manual impl Show for specialized display formats (JSON, XML, domain text)@json.inspect() - use the prelude json_inspect(value, ...) without a package prefixawait keyword; do not add it. Async functions default to raising, so do not add raise; add noraise only 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 the async prefix (e.g. [pub] async fn ..., async test ...).moon Essentialsmoon new my_project - Create new projectmoon run cmd/main - Run main packagemoon run - < hello.mbt - Run code from stdin (useful for quick experiments)moon run -e "code snippet" - Run code from command line argument (good for one-liners)
Example:
cat hello.mbt | moon run -
This allows you to quickly test small snippets of MoonBit code without creating a full project.
It can also be used with heredoc syntax for multi-line snippets:
moon run - <<'EOF'
fn main {
println("Hello, MoonBit!")
}
EOF
moon run -e 'fn main { println("Hello, MoonBit!") }'
moon build - Build project
(moon run and moon build both support --target; moon build also supports --diagnostic-limit <N>)moon check - Type check without building, use it REGULARLY, it is fast
(moon check also supports --target and --diagnostic-limit <N>)moon info - Type check and generate mbti files.
Run it to see if any public interfaces changed.
(moon info also supports --target.)moon check --target all - Type check for all backends
moon check --output-json can be used with jq to filter the output, e.g,
moon check --output-json 2>&1 | jq -R 'fromjson? | select(.message |
contains("unused"))'
or, for richer post-processing, pipe into a small MoonBit program via
moon run -e. Use --target native (the default wasm-gc does not support
async fn main or @stdio.stdin), a quoted heredoc (<<'EOF') so the shell
does not expand $/backticks in the source, and a de-indented closing EOF:
moon check --output-json 2>&1 | moon run --target native -e "$(cat <<'EOF' import { "moonbitlang/async", "moonbitlang/async/stdio", "moonbitlang/core/json", }
async fn main { let seen = {} while @stdio.stdin.read_until("\n") is Some(line) { try @json.parse(line.trim()) catch { _ => () } noraise { {"level": "warning", "path": String(p), ..} => if !seen.contains(p) { seen[p] = () println(p) } _ => () } } } EOF )"
Get the diagnostics with "unused" in the message, which can be used to find unused code.
- `moon explain` - Show built-in documentation for compiler diagnostics.
- `moon explain --diagnostics` lists warning mnemonics and IDs.
- `moon explain --diagnostics 31` explains warning 31 (`unused_optional_argument`).
- `moon explain --diagnostics unused_optional_argument` explains the same warning by mnemonic.
- `moon add package` - Add dependency
- `moon remove package` - Remove dependency
- `moon fmt` - Format code - should be run periodically - note that the files may be rewritten
Note you can also use `moon -C dir check` to run commands in a specific directory.
### Test Commands
- `moon test` - Run all tests
(`moon test` also supports `--target`)
- `moon test --update` - Update snapshots
- `moon test -v` - Verbose output with test names
- `moon test [dirname|filename]` - Test specific directory or file
- `moon coverage analyze` - Analyze coverage
- `moon test [dirname|filename] --filter 'glob'` - Run tests matching filter
```
moon test float/float_test.mbt --filter "Float::*"
moon test float -F "Float::*" // shortcut syntax
```
## `README.mbt.md` Generation Guide
- Output `README.mbt.md` in the package directory.
`*.mbt.md` file and docstring contents treats `mbt check` specially.
`mbt check` block will be included directly as code and also run by `moon check` and `moon test`. If you don't want the code snippets to be checked, explicit `mbt nocheck` is preferred.
If you are only referencing types from the package, you should use `mbt nocheck` which will only be syntax highlighted.
Symlink `README.mbt.md` to `README.md` to adapt to systems that expect `README.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 run `moon test --update` (or `moon test -u`) to fill in `content=`.
- Use `inspect()` for values that implement `Show` (primitives, or types with a manual `impl Show`).
- Use `debug_inspect()` for any type that derives `Debug` — the default for your own data types.
- Use `json_inspect()` for complex nested structures (uses the `ToJson` trait, 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 `Debug` and/or `ToJson` (or `impl Show`) on `YourType` accordingly.
- **Update workflow**: After changing code that affects output, run `moon test --update` to regenerate snapshots, then review the diffs in your test files (the `content=` parameter will be updated automatically).
- **Validation order**: Follow the canonical sequence in `Agent Workflow` and `Fast Task Playbooks`.
- 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 with `ignore(...)` 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 in `catch`, and fail explicitly in `noraise`.
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.
````mbt check
///|
/// Get the largest element of a non-empty `Array`.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(sum_array([1, 2, 3, 4, 5, 6]), content="21")
/// }
/// ```
///
/// # Panics
/// Panics if the `xs` is empty.
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.mbt file (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 declare functions in different files thanks to our package organization.
Run moon test to check everything is correct.
declare is supported for functions, methods, and types.
The pub type Yaml line 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 refactoringFor project-local symbols and navigation, use:
moon ide doc <query> to discover available APIs, functions, types, and methods in MoonBit. Always prefer moon ide doc over other approaches when exploring what APIs are available, it is more powerful and accurate than grep_search or any regex-based searching tools.moon ide outline . to scan a package,moon ide find-references <symbol> to locate usages, andmoon ide peek-def for inline definition context and to locate toplevel symbols.moon ide hover sym --loc filename:line:col to get type information at a specific location.moon ide rename <symbol> <new_name> [--loc filename:line:col] to rename a symbol project-wide. Prefer --loc when 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 (grep displays results in both definitions and call sites including comments too).moon ide doc for API Discoverymoon ide doc uses a specialized query syntax designed for symbol lookup:
Empty query: moon ide doc ''
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"
pkg and list all its exported symbolsmoon ide doc "@json" - explore entire @json packagemoon ide doc "@encoding/utf8" - explore nested packageMultiple queries: moon ide doc "query1" "query2" ...
moon ide doc "String" "Array" "@json" to explore multiple types and a package at onceGlobbing: 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; execution order is defined in Agent Workflow.
moon ide rename sym new_name [--loc filename:line:col] exampleWhen 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 exampleWhen 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] exampleWhen 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 SymbolsUse moon ide outline to scan a package or file for top-level symbols and locate usages without grepping.
moon ide outline dir outlines the current package directory (per-file headers)moon ide outline parser.mbt outlines a single file
This is useful when you need a quick inventory of a package, or to find the right file before peek-def.moon ide find-references TranslationUnit finds 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
moon add moonbitlang/x # Add latest version
moon add moonbitlang/[email protected] # Add specific version
moon update # Update package index
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/[email protected] # browse source/examples without taking a dependency
moon.mod)name = "username/hello"
version = "0.1.0"
readme = "README.mbt.md"
repository = ""
license = "Apache-2.0"
keywords = []
description = "..."
import {
"moonbitlang/[email protected]",
}
options(
// source: "src", // Optional; default is "."
"preferred-target": "native",
)
Use moon add moonbitlang/[email protected] and moon remove moonbitlang/x to manage the import block instead of editing dependency versions by hand.
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.
"module_name/package_path"@alias.function() to call imported functionsliba for username/hello/liba)@packagename in test files to reference the
tested packagePackage Alias Rules:
"username/hello/liba" → use @liba.function() (default alias is the last path segment)import { "moonbitlang/x/encoding" @enc} → use @enc.function()
(Note that this is unnecessary when the last path segment is identical to the alias name.)_test.mbt or _wbtest.mbt files, the package being tested is auto-importedExample:
///|
/// In main.mbt after importing "username/hello/liba" in `moon.pkg`
fn main {
println(@liba.hello()) // Calls hello() from liba package
}
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.
To add a new package fib under .:
Create directory: ./fib/
Add ./fib/moon.pkg
Add .mbt files with your code
Import 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.
Asynchronous programming uses compiler support plus the moonbitlang/async runtime. The runtime supports the native backend best, has limited JavaScript support for IO-independent APIs, and does not support WebAssembly yet. For async IO examples, prefer native. Use moon add moonbitlang/async@<version> and moon ide doc "@async" to explore the API.
User-facing subpackages: @async (core: tasks, timers, cancellation), @async/aqueue, @async/fs, @async/stdio, @async/websocket, ..etc. Each must be imported separately in moon.pkg`.
moon.mod:
import {
"moonbitlang/[email protected]",
}
options(
"preferred-target": "native",
)
moon.pkg, set is-main, restrict to native, and import what you need:
import {
"moonbitlang/async",
"moonbitlang/async/stdio",
}
supported_targets = "native"
options(
"is-main": true,
)
async fn main (not fn main). Spawn concurrent tasks via with_task_group for 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 fn main { ... } or async fn f(...) { ... }, not async fn main raise { ... }.async fn f(...) noraise { ... } only when the async body must not raise. A noraise async function cannot call fallible APIs unless it handles their errors locally.Structured-concurrency contract for with_task_group:
with_task_group returns, every task spawned in the group is guaranteed to have terminated — no orphan tasks, no resource leaks.allow_failure=true), the whole group fails: every other task in the group is cancelled, and the error propagates out of with_task_group.Closure syntax for spawn_bg / spawn:
() => { ... } — idiomatic; async-ness is inferred from context.async fn() { ... } — explicit annotation; equivalent to the arrow form.fn() { ... } — triggers Warning [0027] deprecated_syntax: "this fn is asynchronous but not annotated with async". Don't use.async () => ..., fn() async { ... }, fn(args) async { ... } — all parse errors. async only goes before fn, never before an arrow lambda or after a parameter list.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")
}
await keyword (similar to functions that raise errors). Inside an async test, call async functions normally.async test also has the async raising effect by default; do not add raise.moon test --target native unless moon.mod sets "preferred-target": "native". Use moon test -v when checking test names or async scheduling behavior.README.mbt.md and docstrings, mbt check blocks may contain async test blocks; make sure the package imports moonbitlang/async for the relevant test mode.if, match, loops return values; the last expression is the return value.Ref[T] for primitive mutability.///|. 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).fn is private by default; pub exposes read/construct as allowed; pub(all) allows external construction.import in code files; call via @alias.fn. Configure imports in moon.pkg.... is a valid placeholder in MoonBit code for incomplete implementations.let mut is only needed when you want to reassign a variable, not for mutating fields of a struct or elements of an array/map.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.
raise function: propagate automatically.fn main raise { ... } is valid for synchronous command-line probes and small examples that should propagate errors. For async entry points, use async fn main { ... }; async functions can raise by default.suberror constructors, labeled payloads use label~ : Type; call and pattern-match them with label=value.expr catch { ... } or try { ... } 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.
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 = '𐍈' // ❌ won't compile, '𐍈' is U+10348, which exceeds UInt16 max value 0xffff
let a4 : Byte = b'b' // Byte literal
}
///|
test "bytes literals" {
let b0 : Bytes = b"abcd"
let b1 : Bytes = [0xff, 0x00, 0x01] // Array literal overloading
guard b0 is [b'a', ..] && b0[1] is b'b' else {
// Bytes can be pattern matched as BytesView and indexed
fail("unexpected bytes content")
}
}
///|
test "array literals overloading: disambiguation via type in the current context" {
let (a0, a1, a2, a3) : (
Array[Int],
FixedArray[Int],
ReadOnlyArray[Int],
ArrayView[Int],
) = ([1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3])
// The literal `[1, 2, 3]` is overloaded based on the expected type in the current context.
// Defaults to Array[_]
}
s[i] returns a code unit (UInt16), s.get_char(i) returns Char?.
Since MoonBit supports char literal overloading, you can write code snippets like this:
///|
test "string indexing and utf8 encode/decode" {
let s = "hello world"
let b0 : UInt16 = s[0]
guard b0 is ('\n' | 'h' | 'b' | 'a'..='z') && s is [.. "hello", .. rest] else {
fail("unexpected string content")
}
guard rest is " world" // otherwise will crash (guard without else)
// In check mode (expression with explicit type), ('\n' : UInt16) is valid.
// Using get_char for Option handling
let b1 : Char? = s.get_char(0)
assert_true(b1 is Some('a'..='z'))
// ⚠️ Important: Variables won't work with direct indexing
let eq_char : Char = '='
// s[0] == eq_char // ❌ Won't compile - eq_char is not a literal, lhs is UInt while rhs is Char
// Use: s[0] == '=' or s.get_char(0) == Some(eq_char)
// Requires `"moonbitlang/core/encoding/utf8"` in `moon.pkg`.
let bytes = @utf8.encode("中文")
assert_true(bytes is [0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87])
let s2 : String = @utf8.decode(bytes) // decode utf8 bytes back to String
assert_true(s2 is "中文")
for c in "中文" {
let _ : Char = c // unicode safe iteration
println("char: \{c}") // iterate over chars
}
}
MoonBit uses \{} for string interpolation, for custom types, they need to implement trait Show.
///|
test "string interpolation basics" {
let name : String = "Moon"
let config = { "cache": 123 }
let version = 1.0
println("Hello \{name} v\{version}") // "Hello Moon v1"
// ✅ Quoted map keys are allowed inside interpolation expressions.
println(" - Checking if 'cache' section exists: \{config["cache"]}")
let sb = StringBuilder()
sb <+ "[\{[ for x in [1, 2, 3] => "\{x}" ].join(",")}]"
inspect(sb, content="[1,2,3]")
let x = 42
let streamed = StringBuilder()
streamed <+ "hello \{x}"
inspect(streamed, content="hello 42")
}
Expressions inside \{} must be single-line expressions.
Nested interpolations and string literals are supported, but line breaks inside \{} are not.
<+ and <? macros for streaming interpolationString interpolation can be streamed directly into a Logger/StringBuilder-style writer with <+, or conditionally through an optional writer with <?:
writer <+ "hello \{x}"
writer <+ {"key1": value, "key2": value2}
lhs <? "hello \{x}"
lhs <? {"key1": value, "key2": value2}
This expands to calls on the writer:
writer.write_string("hello ")
writer.write(x)
writer.write_object_begin()
writer.write_object_field("key1", value)
writer.write_object_field("key2", value2)
writer.write_object_end()
if lhs is Some(l) { l <+ "hello \{x}" }
Literal string segments use write_string; interpolated expressions use write.
For <?, None performs no write; Some(writer) applies the same <+ expansion to the wrapped writer.
The right-hand side of <+ and <? must be a template string/multiline template string or a map object literal, not an arbitrary expression.
The expansion is macro-style: it depends on how the writer type implements write_string and write for template strings, plus write_object_begin, write_object_field, and write_object_end for map object literals. Types such as HTMLBuilder or JSONBuilder can support interpolation and streaming with the same syntax but different semantics.
Because MoonBit allows local methods on foreign types, a package can adapt an existing writer type to this syntax by adding those local writer methods.
///|
test "multi-line string literals" {
let multi_line_string : String =
#|Hello "world"
#|World
#|
let multi_line_string_with_interp : String =
$|Line 1 ""
$|Line 2 \{1+2}
$|
// no escape in `#|`,
// only escape '\{..}` in `$|`
assert_eq(multi_line_string, "Hello \"world\"\nWorld\n")
assert_eq(multi_line_string_with_interp, "Line 1 \"\"\nLine 2 3\n")
}
///|
test "map literals and common operations" {
// Map literal syntax
let map : Map[String, Int] = { "a": 1, "b": 2, "c": 3 }
let empty : Map[String, Int] = {} // Empty map, preferred
let also_empty : Map[String, Int] = Map([])
// From array of pairs
let from_pairs : Map[String, Int] = Map::from_array([("x", 1), ("y", 2)])
// Set/update value
map["new-key"] = 3
map["a"] = 10 // Updates existing key
// Get value - returns Option[T]
guard map is { "new-key": 3, "missing"? : None, .. } else {
fail("unexpected map contents")
}
// Direct access (panics if key missing)
let value : Int = map["a"] // value = 10
// Iteration preserves insertion order
for k, v in map {
println("\{k}: \{v}") // Prints: a: 10, b: 2, c: 3, new-key: 3
}
// Other common operations
map.remove("b")
guard map is { "a": 10, "c": 3, "new-key": 3, .. } && map.length() == 3 else {
// "b" is gone, only 3 elements left
fail("unexpected map contents after removal")
}
}
Key Concept: View types (StringView, BytesView, ArrayView[T]) are zero-copy, non-owning read-only slices created with the [:] syntax. They don't allocate memory and are ideal for passing sub-sequences without copying data, for functions which take String, Bytes, Array, they also take *View (implicit conversion).
String → StringView via s[:] or s[start:end] or s[start:] or s[:end]Bytes → BytesView via b[:] or b[start:end], etc.Array[T], FixedArray[T], ReadOnlyArray[T] → ArrayView[T]viaa[:]ora[start:end]`, etc.Important: StringView slice is slightly different due to unicode safety:
s[a:b] may raise an error at surrogate boundaries (UTF-16 encoding edge case). You have two options:
try! s[a:b] if you're certain the boundaries are valid (crashes on invalid boundaries)When to use views:
[first, .. rest])Convert back with .to_string(), .to_bytes(), or .to_array() when you need ownership. (moon ide doc StringView)
enum, struct)///|
enum Tree[T] {
Leaf(T) // Unlike Rust, no comma here
Node(left~ : Tree[T], T, right~ : Tree[T]) // enum can use labels
} derive(Debug, ToJson) // derive traits for Tree
///|
pub fn Tree::sum(tree : Tree[Int]) -> Int {
match tree {
Leaf(x) => x
// we don't need to write Tree::Leaf, when `tree` has a known type
Node(left~, x, right~) => left.sum() + x + right.sum() // method invoked in dot notation
}
}
///|
struct Point {
x : Int
y : Int
} derive(Debug, ToJson) // derive traits for Point
///|
pub fn Point::Point(x~ : Int, y~ : Int) -> Point {
{ x, y }
}
///|
test "user defined types: enum and struct" {
json_inspect(Point(x=10, y=20), content={ "x": 10, "y": 20 })
debug_inspect(
Point(x=10, y=20),
content=(
#|{ x: 10, y: 20 }
),
)
}
for loop///|
pub(all) enum SearchIndex {
Found(Int)
InsertionPoint(Int)
} derive(Debug, Eq)
///|
pub fn binary_search(arr : ArrayView[Int], value : Int) -> SearchIndex {
let len = arr.length()
// functional for loop:
// initial state ; [predicate] ; [post-update] {
// loop body with `continue` to update state
//} nobreak { // exit block
// }
// predicate and post-update are optional
for i = 0, j = len; i < j; {
// post-update is omitted, we use `continue` to update state
let h = i + (j - i) / 2
if arr[h] < value {
continue h + 1, j // functional update of loop state
} else {
continue i, h // functional update of loop state
}
} nobreak { // exit of for loop
if i < len && arr[i] == value {
Found(i)
} else {
InsertionPoint(i)
}
} where {
proof_invariant: 0 <= i && i <= j && j <= len,
proof_invariant: i == 0 || arr[i - 1] < value,
proof_invariant: j == len || arr[j] >= value,
proof_reasoning: (
#|For a sorted array, the boundary invariants are witnesses:
#| - `arr[i-1] < value` implies all arr[0..i) < value (by sortedness)
#| - `arr[j] >= value` implies all arr[j..len) >= value (by sortedness)
#|
#|Preservation proof:
#| - When arr[h] < value: new_i = h+1, and arr[new_i - 1] = arr[h] < value ✓
#| - When arr[h] >= value: new_j = h, and arr[new_j] = arr[h] >= value ✓
#|
#|Termination: j - i decreases each iteration (h is strictly between i and j)
#|
#|Correctness at exit (i == j):
#| - By invariants: arr[0..i) < value and arr[i..len) >= value
#| - So if value exists, it can only be at index i
#| - If arr[i] != value, then value is absent and i is the insertion point
#|
),
}
}
///|
test "functional for loop control flow" {
let arr : Array[Int] = [1, 3, 5, 7, 9]
debug_inspect(binary_search(arr, 5), content="Found(2)") // Array to ArrayView implicit conversion when passing as arguments
debug_inspect(binary_search(arr, 6), content="InsertionPoint(3)")
// for iteration is supported too
for i, v in arr {
println("\{i}: \{v}") // `i` is index, `v` is value
}
}
You are STRONGLY ENCOURAGED to use functional for loops instead of imperative loops
WHENEVER POSSIBLE, as they are easier to read and reason about.
where ClauseThe where clause attaches machine-checkable invariants and human-readable reasoning to functional for loops. This enables formal verification thinking while keeping the code executable. Note for trivial loops, you are encouraged to convert it into for .. in so no reasoning is needed.
Syntax:
for ... {
...
} where {
invariant : <boolean_expr>, // checked at runtime in debug builds
invariant : <boolean_expr>, // multiple invariants allowed
reasoning : <string> // documentation for proof sketch
}
Writing Good Invariants:
Make invariants checkable: Invariants must be valid MoonBit boolean expressions using loop variables and captured values.
Use boundary witnesses: For properties over ranges (e.g., "all elements in arr[0..i) satisfy P"), check only boundary elements. For sorted arrays, arr[i-1] < value implies all arr[0..i) < value.
Handle edge cases with ||: Use patterns like i == 0 || arr[i-1] < value to handle boundary conditions where the check would be out of bounds.
Cover three aspects in reasoning:
continue maintains the invariantsGood example: use labeled and optional parameters
///|
fn g(
positional : Int,
required~ : Int,
optional? : Int, // no default => Option
optional_with_default? : Int = 42, // default => plain Int
) -> String {
// These are the inferred types inside the function body.
let _ : Int = positional
let _ : Int = required
let _ : Int? = optional
let _ : Int = optional_with_default
// `to_repr` (from the prelude `Debug` trait) renders Option via the
// non-deprecated `Show for Repr`, avoiding the deprecated `Show for Option`.
"\{positional},\{required},\{to_repr(optional)},\{optional_with_default}"
}
///|
test {
inspect(g(1, required=2), content="1,2,None,42")
inspect(g(1, required=2, optional=3), content="1,2,Some(3),42")
inspect(g(1, required=4, optional_with_default=100), content="1,4,None,100")
}
Misuse: arg : Type? is not an optional parameter.
Callers still must pass it (as None/Some(...)).
///|
fn with_config(a : Int?, b : Int?, c : Int) -> String {
"\{to_repr(a)},\{to_repr(b)},\{c}"
}
///|
test {
inspect(with_config(None, None, 1), content="None,None,1")
inspect(with_config(Some(5), Some(5), 1), content="Some(5),Some(5),1")
}
Anti-pattern: arg? : Type? (no default => double Option).
If you want a defaulted optional parameter, write b? : Int = 1, not b? : Int? = Some(1).
///|
fn f_misuse(a? : Int?, b? : Int = 1) -> Unit {
let _ : Int?? = a // rarely intended
let _ : Int = b
}
// How to fix: declare `(a? : Int, b? : Int = 1)` directly.
///|
fn f_correct(a? : Int, b? : Int = 1) -> Unit {
let _ : Int? = a
let _ : Int = b
}
///|
test {
f_misuse(b=3)
f_misuse(a=Some(5), b=2) // works but confusing
f_correct(b=2)
f_correct(a=5)
}
Bad example: arg : APIOptions (use labeled optional parameters instead)
///|
/// Do not use struct to group options.
struct APIOptions {
width : Int?
height : Int?
}
///|
fn not_idiomatic(opts : APIOptions, arg : Int) -> Unit {
}
///|
test {
// Hard to use in call site
not_idiomatic({ width: Some(5), height: None }, 10)
not_idiomatic({ width: None, height: None }, 10)
}
A package should own the public concrete types whose constructors, fields, pattern matching, and methods users are expected to use. The owner can be the facade package itself, or a non-internal public package that the facade intentionally re-exports.
Public type ownership is more important than implementation locality. If users think of a type as @foo.X, then X should be
defined in package foo or in a public package re-exported by foo, especially if users call X::method, construct records,
match enum constructors, or rely on generated .mbti docs.
MoonBit can implicitly load the owning package for method lookup when a type is re-exported from a non-internal public package.
For example, if @foo re-exports type X from public package @bar, external users can write a value as @foo.X and still
call methods owned by @bar.X.
Use internal/* packages for implementation support:
Do not put public concrete API types in internal/* and expect a facade to recover the full API with re-exporting. External
users do not get the same implicit method-owner loading for internal packages, so x.method() can fail even when x is typed
as the facade's re-exported type. It also makes constructors, generated interfaces, and privacy boundaries harder to reason
about.
using CorrectlyUse pub using for facade ergonomics, not for type ownership.
Good use:
// root package
pub using @parser { parse, parse_fragment }
pub using @dom { type Node, type NodeKind, to_markdown }
pub using @serializer { type HtmlContext }
This is good when @parser, @dom, and @serializer are public packages that already own those APIs.
Good value re-export from an internal package:
pub using @impl { decode_entities }
This is acceptable if the exported function signature does not expose internal types and you intentionally want that value as public API.
Risky use:
pub using @internal_impl { type X }
Avoid this for public concrete types. If X is public, define it in the facade package or a non-internal public package. If
X is truly internal, do not expose it as a public concrete type.
Use an explicit wrapper instead of pub using when you need to:
.mbti easier to reviewIf a public function returns X, and users should inspect, construct, pattern match, or call methods on X, then X belongs in
the facade package that users name or a non-internal public package that the facade re-exports.
If a helper package only computes data for another package, it may live under internal/*, but its types should either stay
internal or be simple helper result types not exposed through the public facade.
A good package boundary looks like:
foo/
types.mbt // public Foo, FooMode, FooResult
api.mbt // public functions and Foo::methods
private_impl.mbt // private implementation files in same package
internal/foo/
scanner.mbt
escaping.mbt
validation.mbt
development
Guide for migrating OCaml projects, libraries, modules, and test suites to idiomatic MoonBit. Use when translating OCaml code to MoonBit, planning a large OCaml-to-MoonBit port, preserving byte/string-heavy behavior, replacing OCaml variants/records/exceptions/refs/arrays, mapping OCaml APIs to MoonBit packages, or building verification and test strategy for a migration.
development
Refactor MoonBit code to be idiomatic: shrink public APIs, convert functions to methods, use pattern matching with views, add loop invariants, and ensure test coverage without regressions. Use when updating MoonBit packages or refactoring MoonBit APIs, modules, or tests.
development
Use when writing or refactoring proof-carrying code in MoonBit, especially for Why3-backed specifications, abstraction functions, representation invariants, proof assertions, recursive verified data structures, or reducing trusted proof bridges.
documentation
Guide for writing MoonBit bindings to C libraries using native FFI. Use when adding extern "c" declarations, writing C stubs with moonbit.h, configuring native-stub and link.native in moon.pkg, choosing