# Moonbit Practice

> MoonBit code generation and packaging best practices. Use when writing MoonBit code to avoid common AI mistakes with syntax, tests, and benchmarks, or when defining and publishing a Wasm executable with SKILL.md for the MoonBit Skills Marketplace.

- Skill: `mizchi/moonbit-practice` (Agent Skill, multi-file: 19 files)
- Install (CLI): `npx skillmds@latest add mizchi/moonbit-practice`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mizchi/moonbit-practice/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: mizchi (https://skillmd.com/u/mizchi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mizchi/moonbit-practice

---


# 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**.

```bash
# ❌ Avoid: Reading files directly
Read src/parser.mbt

# ✅ Recommended: Find definitions from symbols
moon ide peek-def Parser::parse
moon ide goto-definition -tags 'pub fn' -query 'parse'

# ❌ Avoid: Searching with grep
grep -r "fn parse" .

# ✅ Recommended: Semantic search
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 ide doc` quickly reveals APIs

### Other Rules

- Use `moon ide 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)
- Check reference/skills-marketplace.md before defining or publishing an executable for skills.mooncakes.io; it covers package-local `SKILL.md`, Wasm targets, `moon runwasm`, host capabilities, policy, and release verification

## 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

```moonbit
///| 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

```moonbit
///|
/// 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

```moonbit
///|
/// NG: ! suffix was removed
assert_true!(true)

///|
/// OK
assert_true(true)
```

### Traits and impl require `fn` (v0.10.0)

```moonbit
///| 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`.

```moonbit
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`.

```moonbit
///| 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

```moonbit
let text =
  #|line 1
  #|line 2
```

### Comments and Block Separators

`///|` is a block separator. `///` comments attach to the following `///|` block.

```moonbit
///|
/// 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)`.

```moonbit
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`:

```moonbit
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):

```moonbit

///|
/// 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:

```bash
moon fmt   # Format code
moon info  # Generate type definition files
```

`pkg.generated.mbti` is auto-generated by `moon info`. Don't edit it directly.

## Exploring Built-in Type Methods

```bash
moon ide doc StringView   # StringView methods
moon ide doc Array        # Array methods
moon ide doc Map          # Map methods
```

## Quick Reference

| Topic | Command | Details |
|-------|---------|---------|
| Test | `moon test` | https://docs.moonbitlang.com/en/stable/language/tests |
| Update snapshots | `moon test -u` | Same as above |
| Filtered test | `moon test --filter 'glob'` | Run specific tests |
| Benchmark | `moon bench` | https://docs.moonbitlang.com/en/stable/language/benchmarks |
| Doc Test | `moon check` / `moon test` | https://docs.moonbitlang.com/en/stable/language/docs |
| Format | `moon fmt` | - |
| Generate types | `moon info` | - |
| Doc reference | `moon ide doc <Type>` | - |
| Workspace init | `moon work init` | See reference/configuration.md |
| Workspace add | `moon work use mod1 mod2` | Add modules to workspace |
| API usage analysis | `moon ide analyze .` | Show public API usage stats |

## moon ide Tools

More accurate than grep for code navigation. See `reference/ide.md` for details.

```bash
# Show symbol definition
moon ide peek-def Parser::read_u32_leb128

# Package outline
moon ide outline .

# Find references
moon ide find-references TranslationUnit

# Jump to type definition (with location)
moon ide peek-def Parser -loc src/parse.mbt:46:4

# Show type signature and docs at location (unlike peek-def, shows inferred type + doc comments)
moon ide hover my_func --loc src/lib.mbt:10:4

# Rename symbol across the project
moon ide rename old_name new_name

# Analyze public API usage (v0.8.3+)
moon ide analyze .              # Current package
moon ide analyze internal/*     # Glob pattern
```

## Functional for loop

Prefer functional for loops whenever possible. More readable and easier to reason about.

```moonbit
// 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.

```moonbit
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.

```moonbit
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 ide doc` / docs before relying on capture,
prefix (`before~`), or suffix (`after~`) binding forms.

```moonbit
let matched = s =~ re"abc"
```

## String Constants

`const` supports string concatenation and interpolation (v0.8.3+):

```moonbit
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.

```moonbit
///| 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](https://github.com/moonbit-community/moonbit-overlay) provides a Nix flake overlay for reproducible MoonBit builds.

Minimal `flake.nix` for a MoonBit project:

```nix
{
  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

