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 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 , then moon 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:
// In package B using @A { ... } // re-export A's APIs
Ensure moon check passes before proceeding.
Find and update all call sites:
moon ide find-references <symbol>
Replace bare f with @B.f .
Remove the use statement once all call sites are updated.
Audit and remove newly-unused pub APIs 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 pub from 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:
// Before fn reader_next(r : Reader) -> Char? { ... } let ch = reader_next(r)
// After #as_free_fn(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):
buf..write_string("#\")..write_char(ch)
Prefer Explicit Qualification
-
Use @pkg.fn instead of using when clarity matters.
-
Keep call sites explicit during wide refactors.
Example:
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 more efficient and as 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] => handle_let(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 a_int : Int = 'b' if (a_int is 'a'..<'z') { () } else { () } let a_u16 : UInt16 = 'b' if (a_u16 is 'a'..<'z') { () } else { () } let a_char : Char = 'b' if (a_char 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) => handle_at(rest) Some(Ident(name)) => handle_ident(name) None => () }
Prefer Range Loops for Simple Indexing
-
Use for i in start..<end { ... } , for i in start..<=end { ... } , for i in large>..small , or for i in large>=..small for simple index loops.
-
Keep functional-state for loops for algorithms that update state.
Example:
// Before for i = 0; i < len; { items.push(fill) continue i + 1 }
// 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 xs loops.
-
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 } else { acc } where { invariant: 0 <= i <= xs.length(), reasoning: ( #| ... rigorous explanation ... #| ... ) }
Tests and Docs
-
Prefer black-box tests in *_test.mbt or *.mbt.md .
-
Add docstring tests with mbt check for public APIs.
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 doc "<query>" moon ide outline <dir|file> moon ide find-references <symbol> moon ide peek-def <symbol> moon ide rename <symbol> -new-name <new_name> moon check moon test moon info
Use these commands for reliable refactoring.
Example: spinning off package_b from package_a .
Temporary import in package_b :
using @package_a { a, type B }
Steps:
-
Use moon ide find-references <symbol> to find all call sites of a and B .
-
Replace them with @package_a.a and @package_a.B .
-
Remove the using statement and run moon check .