| name | moonbit-agent-guide |
| description | 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. |
Agent Workflow
For fast, reliable task execution, follow this order:
-
Clarify goal and constraints
- Confirm expected behavior, non-goals, and compatibility constraints (target backend, public API stability, performance limits).
-
Locate module/package boundaries
- Find
moon.mod (module root) and relevant moon.pkg files (package boundaries and imports).
-
Discover APIs before coding
- Prefer
moon ide doc queries to discover existing functions/types/methods before adding new code.
- Use
moon ide outline, moon ide peek-def, and moon ide find-references for semantic navigation.
-
Edit minimally and package-locally
- Keep changes inside the correct package, use
///| top-level delimiters, and split code into cohesive files.
- For refactors, use
moon ide rename; add --loc filename:line:col when names are ambiguous.
- Preserve compatibility with
#alias(old_api, deprecated) when required.
-
Validate in a tight loop
- Run
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).
- Run targeted tests with
moon test [dirname|filename] --filter 'glob' and use moon test --update for snapshot changes.
-
Finalize before handoff
- Run
moon fmt.
- Run
moon info to verify whether public APIs changed (pkg.generated.mbti diff).
- Report changed files, validation commands, and any remaining risks.
Fast Task Playbooks
Use the smallest playbook that matches the request.
Bug Fix (No API Change Intended)
- Reproduce or identify the failing behavior.
- Locate symbols with
moon ide outline, moon ide peek-def, moon ide find-references.
- Implement minimal fix in the current package.
- Validate with:
moon check
moon test [dirname|filename] --filter 'glob' (or closest targeted test scope)
moon fmt
moon info (confirm pkg.generated.mbti unchanged)
Refactor (Behavior Preserving)
- Confirm behavior/API invariants first.
- Prefer semantic rename/navigation tools:
moon ide rename
moon ide find-references
moon ide peek-def
- If multiple symbols share a name, use
moon ide rename <symbol> <new_name> --loc filename:line:col.
- Keep edits package-local and file-organization-focused.
- Validate with:
moon check
moon test [dirname|filename]
moon fmt
moon info (API should remain unchanged unless requested)
New Feature or Public API
- Discover existing idioms with
moon ide doc before introducing new names.
- Add implementation in cohesive files with
///| delimiters.
- Add/extend black-box tests and docstring examples for public APIs.
- Validate with:
moon check
moon test [dirname|filename] (use --update for snapshots when needed)
moon fmt
moon info (review and keep intended pkg.generated.mbti changes)
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.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.
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.
-
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.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.
- It is fine — and encouraged — to have multiple small test files.
-
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.
Do not modify pkg.generated.mbti directly, including whitespace-only cleanup; regenerate it with moon info and review its diff as the public API signal.
For IDE navigation and symbol lookup commands, see the dedicated moon ide section below.
Common Pitfalls to Avoid
- Don't use uppercase for variables/functions - compilation error
- Don't forget
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)
- Don't ignore error handling - either handle errors explicitly, or declare
raise on the caller and let checked errors propagate
- Don't use
return unnecessarily - 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 + 1 or i += 1
- Don't add explicit
try for error propagation - inside a raise function, call error-raising functions normally; use catch to handle locally and try! only when aborting is intended
- Legacy syntax: Legacy code may use
function_name!(...) or function_name(...)? - these are deprecated; use normal calls for propagation.
- Don't write an empty parameter list for
main - use fn main { ... } or fn main raise { ... }, not fn main() { ... } or fn main() raise ... { ... }
- Don't write record-style enum or error constructor fields - labeled constructor fields use
label~ : Type, e.g. InvalidNumber(input~ : String), not InvalidNumber(input: String)
- Prefer range
for loops over C-style - for i in 0..<(n-1) {...} and for j in 0..=6 {...} are more idiomatic in MoonBit
- Don't use
for { ... } for infinite loops - write for ;; { ... } instead
- Don't
derive(Show) for debugging - derive Debug and use debug_inspect() for test/diagnostic output (\{Repr(value)} for interpolation of composed values). Reserve a manual impl Show for specialized display formats (JSON, XML, domain text)
- Don't call
@json.inspect() - use the prelude json_inspect(value, ...) without a package prefix
- Async - MoonBit has no
await 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 Essentials
Essential Commands
moon new my_project - Create new project
moon run cmd/main - Run main package
moon 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!") }'
For multi-line -e snippets, especially snippets with import { ... },
pass real newlines. Do not put literal \n escapes inside single quotes;
MoonBit will see backslash characters, not line breaks. Use command
substitution with a quoted heredoc:
moon run --target native -e "$(cat <<'EOF'
import {
"moonbitlang/x/sys"
}
fn main {
println(@sys.get_cli_args().join("|"))
}
EOF
)"
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 and language topics.
- `moon explain --diagnostic` lists warning mnemonics and IDs.
- `moon explain --diagnostic 31` explains warning 31 (`unused_optional_argument`).
- `moon explain --diagnostic unused_optional_argument` explains the same warning by mnemonic.
- `moon explain --attribute` lists supported attributes such as `#deprecated`, `#alias`, `#cfg`, `#coverage.skip`, and `#warnings`.
- `moon explain --attribute deprecated` explains the `#deprecated` attribute and its supported forms.
- `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.
### 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 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