- name
- moonbit-agent-guide
- description
- Guide for writing, refactoring, and testing MoonBit projects, including exclusive use of `.mbtx` for agent-authored automation. 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, scripting, documentation, and testing conventions.
# MoonBit Task Checklist
- Locate the enclosing `moon.mod` and relevant `moon.pkg` files before editing.
- Discover APIs with `moon ide doc`; use `outline`, `peek-def`, `find-references`, `hover`, and `rename` for semantic navigation and refactoring.
- Use `.mbtx` exclusively 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 check` and the narrowest relevant `moon test`; use `moon test --update` only for intended snapshot changes. Use `moon explain --diagnostic` to list warnings and `--warn-list` to enable selected warnings (for example, `+unnecessary_annotation`).
- Run `moon fmt` and `moon info` before handoff. Review generated `.mbti` changes, 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.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:
1. 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.
2. 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.
3. 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.
4. 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.
5. Tests:
- Place tests in dedicated test files (e.g. `*_test.mbt`) within the appropriate package.
For a package (besides `*_test.mbt`files), `*.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.
6. 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
## 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:
```bash
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 add `raise` or `await`.
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`):
```mbtx
fn main {
println("Hello from MoonBit script mode")
}
```
Command-line automation (`sum_args.mbtx`):
```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`):
```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:
```mbtx
import {
"moonbitlang/async",
}
async fn main {
@async.sleep(10)
println("async automation complete")
}
```
## Essential Commands
- `moon new my_project` - Create new project
- `moon run cmd/main` - Run main package
- `moon 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 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
Process structured diagnostics with a saved `.mbtx` script rather than a
shell pipeline or another scripting language. For example, save this as
`filter_diagnostics.mbtx`; `@shell.Cmd::each_line` runs `moon check` directly,
streams its JSON output, and returns its exit status:
```mbtx
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"]
}
]
}
}
```
```bash
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 --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
Ver en GitHub