SKILL.md
MoonBit Refactoring Skill
Intent
- Preserve behavior and public contracts unless explicitly changed.
- Minimize the public API to what callers require.
- Prefer declarative style and pattern matching over incidental mutation.
- Use view types (ArrayView/StringView/BytesView) to avoid copies.
- Add tests and docs alongside refactors.
Workflow
Start broad, then refine locally:
- Architecture first: Review package structure, dependencies, and API boundaries.
- Inventory public APIs and call sites (
moon ide doc,moon ide find-references). - Pick one refactor theme (API minimization, package splits, pattern matching, loop style).
- Apply the smallest safe change.
- Update docs/tests in the same patch.
- Run
moon check, thenmoon test. - Use coverage to target missing branches.
Avoid local cleanups (renaming, pattern matching) until the high-level structure is sound.
Improve Package Architecture
- Keep packages focused: aim for <10k lines per package.
- Keep files manageable: aim for <2k lines per file.
- Keep functions focused: aim for <200 lines per function.
Splitting Files
Treat files in MoonBit as organizational units; move code freely within a package as long as each file stays focused on one concept.
Splitting Packages
When spinning off package A into A and B:
- Create the new package and re-export temporarily:
``mbt // In package B using @A { ... } // re-export A's APIs ` Ensure moon check` passes before proceeding.
- Find and update all call sites:
``bash moon ide find-references <symbol> ` Replace bare f with @B.f`.
- Remove the
usestatement once all call sites are updated.
- Audit and remove newly-unused
pubAPIs from both packages.
Guidelines
- Prefer acyclic dependencies: lower-level packages should not import higher-level ones.
- Only expose what downstream packages actually need.
- Consider an
internal/package for helpers that shouldn't leak.
Minimize Public API and Modularize
- Remove
pubfrom helpers; keep only required exports. - Move helpers into
internal/packages to block external imports. - Split large files by feature; files do not define modules in MoonBit.
Local refactoring
Convert Free Functions to Methods + Chaining
- Move behavior onto the owning type for discoverability.
- Use
..for fluent, mutating chains when it reads clearly.
Example: ```mbt nocheck // Before fn readernext(r : Reader) -> Char? { ... } let ch = readernext(r)
// After #asfreefn(reader_next, deprecated="Use Reader::next instead") fn Reader::next(self : Reader) -> Char? { ... } let ch = r.next()
To make the transition smooth, place `#as_free_fn(old_name, ...)` on the method; it emits a deprecated free function
`old_name` that forwards to the method.
Then you can check call sites and update them gradually by looking at warnings.
Example (chaining):
```mbt nocheck
buf..write_string("#\\")..write_char(ch)
Prefer Explicit Qualification
- Use
@pkg.fninstead ofusingwhen clarity matters. - Keep call sites explicit during wide refactors.
Example: ```mbt nocheck let n = @parser.parse_number(token)
### Simplify Enum Constructors When Type Is Known
When the expected type is known from context, you can omit the full package path for enum constructors:
- **Pattern matching**: Annotate the matched value; constructors need no path.
- **Nested constructors**: Only the outermost needs the full path.
- **Return values**: The return type provides context for constructors in the body.
- **Collections**: Type-annotate the collection; elements inherit the type.
Examples:
// Pattern matching - annotate the value being matched let tree : @pkga.Tree = ... match tree { Leaf(x) => x Node(left~, x, right~) => left.sum() + x + right.sum() }
// Nested constructors - only outer needs full path let x = @pkga.Tree::Node(left=Leaf(1), x=2, right=Leaf(3))
// Return type provides context fn make_tree() -> @pkga.Tree { Node(left=Leaf(1), x=2, right=Leaf(3)) }
// Collections - type annotation on the array let trees : Array[@pkga.Tree] = [Leaf(1), Node(left=Leaf(2), x=3, right=Leaf(4))]
### Pattern Matching and Views
- Pattern match arrays directly; the compiler inserts ArrayView implicitly.
- Use `..` in the middle to match prefix and suffix at once.
- Pattern match strings directly; avoid converting to `Array[Char]`.
- `String`/`StringView` indexing yields `UInt16` code units. Use `for ch in s` for Unicode-aware iteration.
#### We Prefer Pattern Matching Over Small Functions
For example,
match gen_results.get(0) { Some(value) => Iter::singleton(value) None => Iter::empty() }
We can pattern match directly; it is often clearer and equally readable:
match gen_results { [value, ..] => Iter::singleton(value) [] => Iter::empty() }
MoonBit pattern matching is pretty expressive, here are some more examples:
match items { [] => () [head, ..tail] => handle(head, tail) [..prefix, mid, ..suffix] => handle_mid(prefix, mid, suffix) }
match s { "" => () [.."let", ..rest] => handlelet(rest) => () }
#### Char literal matching
Use char literal overloading for `Char`, `UInt16`, and `Int`; the examples below rely on it. This is handy when matching `String` indexing results (`UInt16`) against a char range.
test { let aint : Int = 'b' if (aint is 'a'..<'z') { () } else { () } let au16 : UInt16 = 'b' if (au16 is 'a'..<'z') { () } else { () } let achar : Char = 'b' if (achar is 'a'..<'z') { () } else { () } }
#### Use Nested Patterns and `is`
- Use `is` patterns inside `if`/`guard` to keep branches concise.
Example:
match token { Some(Ident([.."@", ..rest])) if process(rest) is Some(x) => handleat(rest) Some(Ident(name)) => handleident(name) None => () }
### Prefer Functional Loops to Mutation When Possible
- Use functional state update
Example:
// Before let mut a = 1 let mut b = 2 for i = 0 { if i >= n { break } a = a + b b = b + a continue i + 1 }
```mbt nocheck
for i = 0, a = 1, b = 2 {
if i >= n {
break a
}
continue i + 1, b, b + a
}
- Functional loops also accept range loops, so they can be simplified:
for _ in 0..<n; a = 1, b = 2 {
continue b, a + b
} nobreak {
a
}
Prefer Range Loops to Simple Indexing
- Use
for i in start..<end { ... },for i in start..<=end { ... },for i in large>..small, orfor i in large>=..smallfor simple index loops. - Use
for x in xs {...}if xs is an iterator(e.g, Array) orfor i,x in xs {...}if you also want to use the index - Keep functional-state
forloops for algorithms that update state.
Example:
// Before
for i = 0; i < len; i = i + 1{
items.push(fill)
}
// After
for i in 0..<len {
items.push(fill)
}
Loop Specs (Dafny-Style Comments)
- Add specs for functional-state loops.
- Skip invariants for simple
for x in xsloops. - Add TODO when a decreases clause is unclear (possible bug).
Example:
for i = 0, acc = 0; i < xs.length(); {
acc = acc + xs[i]
i = i + 1
} nobreak { acc }
where {
invariant: 0 <= i <= xs.length(),
reasoning: (
#| ... rigorous explanation ...
#| ...
)
}
Tests and Docs
- Prefer black-box tests in
_test.mbtor.mbt.md. - Add docstring tests using
mbt checkfenced blocks for public APIs, and verify withmoon check && moon test.
Example:
///|
/// Return the last element of a non-empty array.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(last([1, 2, 3]), content="3")
/// }
/// ```
pub fn last(xs : Array[Int]) -> Int { ... }
Coverage-Driven Refactors
- Use coverage to target missing branches through public APIs.
- Prefer small, focused tests over white-box checks.
Commands:
moon coverage analyze -- -f summary
moon coverage analyze -- -f caret -F path/to/file.mbt
Moon IDE Commands
moon ide doc "<query>"
moon ide outline <dir|file>
moon ide find-references <symbol>
moon ide peek-def <symbol>
moon ide rename <symbol> <new_name>
moon ide analyze [path]
moon check
moon test
moon info
Use these commands for reliable refactoring.
Example: spinning off packageb from packagea.
Temporary import in package_b:
using @package_a { a, type B }
Steps:
- Use
moon ide find-references <symbol>to find all call sites ofaandB. - Replace them with
@packagea.aand@packagea.B. - Remove the
usingstatement and runmoon check.