| name | moonbit-practice |
| description | MoonBit code generation best practices. Use when writing MoonBit code to avoid common AI mistakes with syntax, tests, and benchmarks. |
MoonBit Practice Guide
Best practices for AI when generating MoonBit code.
If you don't understand something here, use moonbit-docs skill to search the official documentation.
Guidelines
Code Navigation: Prefer moon ide over Read/Grep
In MoonBit projects, prefer moon ide commands over Read tool or grep.
Read src/parser.mbt
moon ide peek-def Parser::parse
moon ide goto-definition -tags 'pub fn' -query 'parse'
grep -r "fn parse" .
moon ide find-references parse
moon ide outline src/parser.mbt
Why:
moon ide provides semantic search (distinguishes definitions from call sites)
- grep picks up comments and strings
moon doc quickly reveals APIs
Other Rules
- Use
moon doc '<Type>' to explore APIs before implementing
- Config format: prefer the DSL
moon.mod / moon.pkg over the deprecated-pending moon.mod.json / moon.pkg.json. moon fmt migrates JSON → DSL. Samples: assets/moon.mod, assets/moon.pkg. Check reference/configuration.md before editing either
- Multiple modules in one repo → use a workspace (
moon.work), not standalone modules. moon work init <dirs> creates the manifest; members share one build context and resolve each other locally. See reference/configuration.md "Workspace"
- Check reference/language.md for detailed language feature examples (types, traits, pattern matching, etc.)
- Check reference/mbtx.md for single-file
.mbtx scripts (moon run script.mbtx, stdin via moon run -, inline import { } block; nightly only)
Common Pitfalls
- Don't use uppercase for variables/functions - compilation error
mut is only for reassignment, not field mutation - Array push doesn't need it
return is unnecessary - last expression is the return value
- Methods require
Type:: prefix
++ -- not supported - use i = i + 1 or i += 1
- No
try needed for error propagation - automatic (unlike Swift)
- No
await keyword - just declare with async fn
- Prefer range for over C-style -
for i in 0..<n {...}
nobreak not else for functional for-loop exit values (else is deprecated)
- Legacy syntax:
function_name!(...) and function_name(...)? are deprecated
for { ... } is deprecated - use for ;; { ... } or while true { ... } for infinite loops
- Cross-package
. syntax for impl removed - . method call only works within the same package
trait/impl methods now require the fn keyword (v0.10.0) - moon fmt migrates old code; see "Traits and impl" below
try? is deprecated (v0.10.0) - use Ok(expr) catch { e => Err(e) } to get a Result
- Struct
fn new(..) constructor removed (v0.10.0) - use a user-defined constructor fn Type::Type(..)
immut/array replaced by immut/vector (v0.9) - update imports/deps; better performance
inspect superseded by debug_inspect for snapshot tests (v0.9.2) - Show for container types (Array/Map/Set/Option/Result/tuples) is deprecated; see "Snapshot Tests"
Common Syntax Mistakes by AI
Type Parameter Position
///| NG: fn identity[T] is old syntax
fn identity[T](val: T) -> T { val }
///| OK: Type parameter comes right after fn
fn[T] identity(val: T) -> T { val }
raise Syntax
///|
/// NG: -> T!Error was removed
fn parse(s: String) -> Int!Error { ... }
///|
/// OK: Use raise keyword
fn parse(s: String) -> Int raise Error { ... }
Int raise is shorthand for Int raise Error.
async fn implicitly raises by default; use noraise to enforce no errors.
Macro Calls
///|
/// NG: ! suffix was removed
assert_true!(true)
///|
/// OK
assert_true(true)
Traits and impl require fn (v0.10.0)
///| NG: method without fn (deprecated, warns then removed)
trait I {
f(Self) -> Unit
}
impl I for Int with f(_) {}
///| OK: fn keyword on both the trait method and the impl
trait I {
fn f(Self) -> Unit
}
impl I for Int with fn f(_) {}
Polymorphic trait methods: the method's own type parameters go after fn,
the impl's own type parameters stay after impl.
trait Logger {
fn[X : Show] write_object(Self, X) -> Unit
}
impl Logger for StringBuilder with fn write_object(self, x) {
self.write_string(x.to_string())
}
impl[A] Poly for Array[A] with fn[X] f(self, x : X) { ... }
// ^^^ impl type params ^^^ method type params
moon fmt rewrites old trait/impl syntax automatically.
Structs, Constructors, Methods
The removed fn new(..) special form is replaced by a plain user-defined
constructor fn Type::Type(..). Methods take the receiver as self : Self.
///| Struct with derives
struct Point {
x : Int
y : Int
} derive(Eq, Debug)
///| User-defined constructor (replaces removed `fn new`)
fn Point::Point(x : Int, y : Int) -> Point {
{ x, y } // field-punning struct literal
}
///| Method: receiver `self : Self`, last expression is the return value
fn Point::manhattan(self : Self) -> Int {
self.x.abs() + self.y.abs()
}
Multi-line Text
let text =
#|line 1
#|line 2
Comments and Block Separators
///| is a block separator. /// comments attach to the following ///| block.
///|
/// This function is foo
fn foo() -> Unit { ... }
///|
/// This function is bar
fn bar() -> Unit { ... }
Avoid consecutive ///| at the file beginning as they create separate blocks.
Snapshot Tests
moon test -u auto-fills the empty content="" of inspect(val) /
debug_inspect(val). Which to use (v0.9.2+):
- Primitives / strings →
inspect (Show is fine here)
- Container types (
Array/Map/Set/Option/Result/tuples) and custom
types → debug_inspect, because Show for these containers is deprecated and
Debug gives structured, indented output. Keep Show for human-facing
formatting only.
Choose by the inspected expression's static type, not the runtime value:
Result[Int, _] is a container even when it holds Ok(123), and the rule
propagates — to snapshot/assert a custom type (even nested inside a container) the
custom type must derive(Debug), and equality via @debug.assert_eq additionally
needs derive(Eq).
test "snapshot" {
inspect("hello", content="") // primitive/string -> inspect
debug_inspect([1, 2, 3], content="") // container -> debug_inspect
}
///| Custom type: derive Debug to snapshot, derive Eq to assert_eq
struct P {
x : Int
} derive(Eq, Debug)
test "custom type" {
debug_inspect(P::{ x: 1 }, content="")
@debug.assert_eq(P::{ x: 1 }, P::{ x: 1 })
}
After moon test -u:
test "snapshot" {
inspect("hello", content="hello")
debug_inspect([1, 2, 3], content="[1, 2, 3]")
}
Doc Tests
Available in .mbt.md files or ///| inline comments.
| Code Block | Behavior |
|---|
```mbt check | Checked by LSP |
```mbt test | Executed as test {...} |
```moonbit | Display only (not executed) |
Example (inline comment):
///|
/// Increment an integer by 1
/// ```mbt test
/// inspect(incr(41), content="42")
/// ```
pub fn incr(x : Int) -> Int {
x + 1
}
Pre-release Checklist
Run before releasing:
moon fmt
moon info
pkg.generated.mbti is auto-generated by moon info. Don't edit it directly.
Exploring Built-in Type Methods
moon doc StringView
moon doc Array
moon doc Map
Quick Reference
moon ide Tools
More accurate than grep for code navigation. See reference/ide.md for details.
moon ide peek-def Parser::read_u32_leb128
moon ide outline .
moon ide find-references TranslationUnit
moon ide peek-def Parser -loc src/parse.mbt:46:4
moon ide hover my_func --loc src/lib.mbt:10:4
moon ide rename old_name new_name
moon ide analyze .
moon ide analyze internal/*
Functional for loop
Prefer functional for loops whenever possible. More readable and easier to reason about.
// Functional for loop with state
for i = 0, sum = 0; i <= 10 {
continue i + 1, sum + i // Update state
} nobreak {
sum // Value at loop exit (nobreak, not else)
}
// Range for (recommended)
for i in 0..<n { ... }
for i, v in array { ... } // index and value
// Range for with extra loop variable (v0.8.3+)
for x in xs; sum = 0 {
continue sum + x
} nobreak {
sum
}
// Infinite loop (for { } is deprecated)
for ;; { ... }
List Comprehensions (v0.9.2+)
[for .. => body] builds an array; add if <guard> before => to filter.
The same shape typed as Iter[T] yields a lazy iterator instead of an array.
let evens = [for i in 0..<100 if i % 2 == 0 => i]
///| With loop state (v0.10.0): carries p1/p2 across iterations
let fibs = [
for _ in 0..<10
p1 = 1, p2 = 0
p1 = p1 + p2, p2 = p1 => p1
]
///| Typed as Iter -> lazy
let fib_iter : Iter[Int] = [for p1 = 1, p2 = 0;; p1 = p1 + p2, p2 = p1 => p1]
Template Write <+ (v0.10.0)
buf <+ expr appends to a StringBuilder — terser than write_string, pairs
well with $| interpolation for building markup/strings.
let buf = StringBuilder()
buf <+ "<ul>"
for item in items {
buf <+ $|<li>\{item}</li>
}
buf <+ "</ul>"
buf.to_string()
Regex Match =~ (v0.9+)
Stable regex matching (s =~ re"...") replaces the experimental lexmatch?,
with different semantics — check moon doc / docs before relying on capture,
prefix (before~), or suffix (after~) binding forms.
let matched = s =~ re"abc"
String Constants
const supports string concatenation and interpolation (v0.8.3+):
const Hello : String = "Hello"
const HelloWorld : String = Hello + " world"
const Message : String =
$|========
$|\{HelloWorld}
$|========
Error Handling
MoonBit uses checked errors. See reference/ffi.md for details.
///| Declare error type
suberror ParseError {
InvalidEof
InvalidChar(Char)
}
///| Declare with raise, auto-propagates
fn parse(s: String) -> Int raise ParseError {
if s.is_empty() { raise ParseError::InvalidEof }
...
}
///| Convert to Result — `try?` is deprecated (v0.10.0)
let result : Result[Int, ParseError] = Ok(parse(s)) catch { e => Err(e) }
///| Single expression with a fallback: omit `try`
let n = parse(s) catch { _ => 0 }
///| Full form: `noraise` branch handles the success value
let n = try parse(s) catch {
ParseError::InvalidEof => -1
_ => 0
} noraise {
v => v
}
CI and Publishing
Third-party actions
Prefer the official curl installer (assets/ci.yaml) or Nix with moonbit-overlay (assets/ci-nix.yaml) over third-party actions such as hustcer/setup-moonbit@v1. The installer is short and adds nothing to the action supply chain; the Nix path is fully reproducible when a flake.nix is present.
moon update is mandatory
Runners start with an empty registry index. Any registry-hosted MoonBit dependency makes moon check / moon test / moon build fail with Failed to resolve registry dependency until you run moon update. Put it immediately after installing the CLI in every workflow that touches mooncakes.
Publishing an npm package whose build depends on MoonBit
Some projects (e.g. Vite plugins, language tooling) ship as npm packages but run moon inside pnpm build — for instance pnpm build:parser that calls moon -C tools/parser build --release --target js. In that case the npm publish workflow needs both the MoonBit CLI and moon update before pnpm build, otherwise the build fails on CI.
See assets/publish-to-npm.yaml for a minimal release-triggered publish workflow that uses OIDC Trusted Publishing (no NPM_TOKEN) and sets up MoonBit correctly. Pair it with a release automation tool (see the npm-release skill for a release-please + OIDC setup) to avoid manual npm publish calls.
Assets
assets/moon.mod — module config sample in the new DSL (replaces moon.mod.json)
assets/moon.pkg — package config sample in the new DSL (replaces moon.pkg.json)
assets/ci.yaml — GitHub Actions CI (curl installer)
assets/ci-nix.yaml — GitHub Actions CI with Nix (moonbit-overlay)
assets/publish-to-npm.yaml — release-triggered npm publish with MoonBit build and OIDC Trusted Publishing
Nix Setup (moonbit-overlay)
moonbit-community/moonbit-overlay provides a Nix flake overlay for reproducible MoonBit builds.
Minimal flake.nix for a MoonBit project:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
moonbit-overlay.url = "github:moonbit-community/moonbit-overlay";
moon-registry = {
url = "git+https://mooncakes.io/git/index";
flake = false;
};
};
outputs = { nixpkgs, moonbit-overlay, moon-registry, ... }:
let
system = "x86_64-linux"; # or aarch64-darwin, etc.
pkgs = import nixpkgs {
inherit system;
overlays = [ moonbit-overlay.overlays.default ];
};
moonHome = pkgs.moonPlatform.bundleWithRegistry {
cachedRegistry = pkgs.moonPlatform.buildCachedRegistry {
moonModJson = ./moon.mod.json;
registryIndexSrc = moon-registry;
};
};
in {
devShells.${system}.default = pkgs.mkShellNoCC {
packages = [ moonHome pkgs.git ];
env.MOON_HOME = "${moonHome}";
};
};
}
Key APIs from the overlay:
pkgs.moonPlatform.buildMoonPackage - Build a MoonBit package as a Nix derivation
pkgs.moonPlatform.bundleWithRegistry - Create a MOON_HOME with cached registry
pkgs.moonPlatform.buildCachedRegistry - Pre-fetch mooncakes registry dependencies