moonbitlang/moonbit-agent-guide/ocaml2moonbit-migration

Claude Code and agent skill by moonbitlang/moonbit-agent-guide

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Guide for migrating OCaml projects, libraries, modules, and test suites to idiomatic MoonBit. Use when translating OCaml code to MoonBit, planning a large OCaml-to-MoonBit port, preserving byte/string-heavy behavior, replacing OCaml variants/records/exceptions/refs/arrays, mapping OCaml APIs to MoonBit packages, or building verification and test strategy for a migration.

Install

npx vskill@latest install moonbitlang/moonbit-agent-guide/ocaml2moonbit-migration

SKILL.md

OCaml to MoonBit Migration

Port behavior, data invariants, and public contracts first. Translate syntax only after the source semantics are classified. The most common porting bug is silently coercing OCaml string (a byte sequence) into MoonBit String (UTF-16 text); classify every field by meaning before choosing a type.

When in doubt, probe with moon run -c '...'. Probes in this guide were verified on moon 0.1.20260512-class toolchains; rerun the relevant probe if the local toolchain is newer and the behavior is load-bearing.

Migration Workflow

  1. Inventory the OCaml module boundary: public types, functions, exceptions, optional arguments, mutable state, lazy/deferred state, C/Unix/filesystem dependencies, and existing tests or golden fixtures.
  2. Classify every OCaml string by meaning before choosing a MoonBit type. Do this field by field, even inside the same OCaml record.
  3. Choose MoonBit package boundaries and imports before coding. Add imports to moon.pkg; MoonBit source files do not use OCaml-style open.
  4. Port one behavioral slice at a time with tests. Prefer a thin public API skeleton, then fill parser/serializer/algorithm internals behind it.
  5. Probe uncertain language or library behavior with moon run -c and, when needed, a small OCaml toplevel probe. Keep probes minimal.
  6. Finish each slice with moon check, moon test, moon info, and moon fmt. You can add more warnings moon check --warn-list +... to be more strict.

Type-Mapping Cheatsheet

OCaml useMoonBit default
binary payload, file contents, compressed/encrypted/checksummed data, parser inputBytes or BytesView
human-readable text, diagnostics, labels that are truly UnicodeString
single byte with known 0..255 rangeByte
indexes, counts, small identifiers, deliberate signed 32-bit wrappingInt
file offsets, serialized positions, large object numbersInt64 or UInt64
OCaml array with fixed lengthFixedArray[T]
mutable growable builderArray[T]
read-only sequence parameterArrayView[T] or BytesView
compile-time lookup table (literal or comprehension)ReadOnlyArray[T]
keyed lookup with deterministic iterationMap[K, V]
OCaml Buffer.t byte builder@buffer.Buffer
OCaml refRef[T]
OCaml variantenum, often priv enum for internal states
OCaml recordstruct, with { ..old, field: value } for immutable update
OCaml exception flowsuberror plus checked raise
lazy/deferred stateLazy[T]
OCaml int32 needing wrappingInt
OCaml int32 needing wide arithmeticInt64 or UInt64
OCaml floatDouble

There is no Int32 type in the current toolchain.

Bytes vs String

OCaml String.length counts bytes; MoonBit String::length() counts UTF-16 code units. The same source character takes different positions in the two languages, and that difference is silent.

sh
ocaml -noprompt -noinit <<'EOF'
let s = "𝄞";;
Printf.printf "%d\n" (String.length s);;
Printf.printf "%d\n" (Char.code s.[0]);;
EOF
# 4
# 240
sh
moon run -c 'fn main { let s = "𝄞"; println(s.length()); println(s.char_length()) }'
# 2
# 1

String::length() is UTF-16 code units, String::char_length() is Unicode scalars, and neither is bytes. For byte-oriented formats use Bytes/BytesView. Convert between Bytes and String only through a named encoding helper (@ascii.encode, @utf8.encode, etc.) that documents the encoding assumption.

sh
moon run -c 'fn main { let raw : Array[Byte] = [65, 0, 255]; let b = Bytes::from_array(raw); println(b.length()); println(b[2].to_int()) }'
# 3
# 255
sh
moon run -c 'fn main { let source : Array[Byte] = [1, 2]; let bytes = Bytes::from_array(source); source[0] = 9; println(bytes[0].to_int()); println(source[0].to_int()) }'
# 1
# 9

Bytes::from_array produces an immutable owned Bytes. Mutating the source array afterwards does not affect the frozen value. Port OCaml Bytes mutators either as BytesView -> Bytes transforms, or keep state in Array[Byte]/FixedArray[Byte] until the final freeze.

sh
moon run -c 'fn main { let empty = Bytes::new(0); let zeros = Bytes::new(2); println(empty.length()); println(zeros.length()); println(zeros[0].to_int()) }'
# 0
# 2
# 0

Bytes::new(length) always zero-fills and requires an explicit length. Use it where OCaml Bytes.empty or Bytes.make len '\000' would have appeared.

Bytes::copy is deprecated because Bytes is immutable. When a port must materialize a fresh physical copy (e.g. to preserve an OCaml promise that no buffer is shared), use Bytes::makei(len, i => src[i]).

sh
moon run -c $'let cached_filter = b"/Filter"\nfn main { let b = b"PDF"; println(b.length()); println(b[0].to_int()); println(cached_filter.length()); println(cached_filter[0].to_int()) }'
# 3
# 80
# 7
# 47

b"..." is a compile-time byte-string literal of type Bytes. Use it for ASCII format syntax (b"/Filter", b"PDF", etc.) — no @ascii import, no runtime call, no type annotation needed at top level. Reserve @ascii.encode(text) for String → Bytes conversion when the source is a dynamic String. Do not build binary file formats by String concatenation.

sh
moon run -c $'fn main { let buf = @buffer.new(); buf.write_bytes(b"PDF"); buf.write_byte(10); let bytes = buf.contents(); println(bytes.length()); println(bytes[0].to_int()); println(bytes[3].to_int()) }'
# 4
# 80
# 10

The canonical MoonBit byte-builder is @buffer.Buffer (from moonbitlang/core/buffer). Construct with @buffer.new() (optionally size_hint=N), append static ASCII via buf.write_bytes(b"..."), single bytes via buf.write_byte(n), dynamic text via buf.write_bytes(@ascii.encode(text)), binary payloads via buf.write_bytes(view), then freeze with buf.contents(). Reserve Array[Byte] plus Bytes::from_array for cases that need random-access mutation of in-flight bytes; Buffer is the OCaml Buffer.t analogue.

sh
moon run -c 'fn main { let s = "/UniJIS-UCS2-H"; println(s.has_prefix("/Uni")); println(s.contains("-UCS2-")); println(s.has_suffix("-H")); println(s[1:]); println(s[1:].to_owned()) }'
# true
# true
# true
# UniJIS-UCS2-H
# UniJIS-UCS2-H

String::has_prefix, has_suffix, and contains cover predicate work. Slice with s[start:end] — like BytesView, this returns a borrowed StringView, cheap and good for inspection/pattern matching. When the callee needs an owned String (e.g. for storage or a String parameter), use s[start:end].to_owned(). Offsets are UTF-16 code-unit offsets, not bytes; use these only for ASCII-validated tokens or genuinely textual parsing.

Bytes Views

sh
moon run -c 'fn has_ab(view : BytesView) -> Bool { match view { [65, 66, ..] => true; _ => false } }
fn main { let bytes : Bytes = [65, 66, 67]; let view = bytes[:2]; println(view.length()); println(view[0].to_int()); println(has_ab(bytes[:])); println(has_ab(view)) }'
# 2
# 65
# true
# true

BytesView is the byte-sequence counterpart to ArrayView. Views are cheap slices, expose read-only byte operations, and support pattern matching with rest patterns. Prefer BytesView for read-only byte APIs; call .to_owned() only at explicit ownership boundaries. BytesView::to_owned() returns the original bytes for a whole view but allocates and copies for a partial slice.

Bytes is assignable to a BytesView parameter, so a single API can accept owned or borrowed input. A returned BytesView keeps its backing Bytes alive across function boundaries — useful for exposing decode/decrypt results without forcing a copy.

The reverse direction is not automatic: a BytesView does not type-check where owned Bytes is required, including in native extern declarations. Treat that as the ownership boundary and call .to_owned() deliberately. Equality works when the view is the left operand; if Bytes is on the left, slice it (bytes[:]) before comparing.

sh
moon run -c 'fn main { let data : Bytes = [60, 65, 62, 0, 12, 60, 66, 62]; println(data.length()); println(data[3].to_int()); println(data[4].to_int()); let view = data[5:]; match view { [60, 66, 62] => println("match"); _ => println("miss") } }'
# 8
# 0
# 12
# match

NUL (0) and form-feed (12) are ordinary bytes in Bytes/BytesView. Port OCaml byte predicates literally; do not narrow a format whitespace predicate to a string-oriented space/tab check.

Integers

sh
moon run -c 'fn main { let (b, u, u16, i64, u64) : (Byte, UInt, UInt16, Int64, UInt64) = (255, 7, 65535, 42, 42); println(b.to_int()); println(u.to_string()); println(u16.to_int()); println(i64.to_string()); println(u64.to_string()) }'
# 255
# 7
# 65535
# 42
# 42

Scalar types: Byte, Int16, UInt16, Int, UInt, Int64, UInt64, Float, Double. There is no Int32; use Int for deliberate signed 32-bit wrapping, or Int64/UInt64 when an OCaml int32 value must be represented without truncation. When calling a method on an integer literal, parenthesize: (65).to_byte(), not 65.to_byte() — 65. parses as the start of a float.

sh
moon run -c 'fn main { let max = 2147483647; println(max + 1); println(1 << 31); println(0xF0 & 0x0F); println(0x80 >> 7); println(0xAA ^ 0xFF) }'
# -2147483648
# -2147483648
# 0
# 1
# 85

Int arithmetic wraps modulo 2^32 with signed interpretation. Symbolic operators map directly from OCaml: & (land), | (lor), ^ (lxor), << (lsl), >> (right shift, arithmetic for signed).

sh
moon run -c 'fn main { println((-8) >> 1); println(1 << 32); println(1 >> 32); let logical = ((-8).reinterpret_as_uint() >> 1).reinterpret_as_int(); println(logical) }'
# -4
# 1
# 1
# 2147483644

Int right shift is arithmetic, and shift counts are masked to 5 bits, so 1 << 32 == 1. For OCaml Int32.shift_right_logical, reinterpret signed→unsigned, shift, reinterpret back. Numeric conversion (.to_uint()) is not the same as bit reinterpretation (.reinterpret_as_uint()).

sh
moon run -c $'fn main { try @string.parse_int("2147483648") catch { _ => println(true) } noraise { _ => println(false) }; let mut value = 0; for digit in [50,49,52,55,52,56,51,54,52,56] { value = value * 10 + digit - 48 }; println(value); let mut wide = 0L; for digit in [50,49,52,55,52,56,51,54,52,56] { wide = wide * 10L + (digit - 48).to_int64() }; println(wide.to_string()) }'
# true
# -2147483648
# 2147483648

@string.parse_int rejects out-of-range decimals, but handwritten digit accumulation in Int silently wraps. Accumulate offsets, lengths, object numbers, or serialized counters in Int64/UInt64; bounds-check before narrowing to Int.

sh
moon run -c 'fn main { println(0x8EA2A1A1 < 0); println(0x8EA2A1A1); println(0x7FFFFFFF < 0x8EA2A1A1); println((-1) % 256) }'
# true
# -1901944415
# false
# -1

Int literals above 0x7FFFFFFF are negative. A table sorted by unsigned byte order is not sorted for signed Int comparison — compare through UInt (reinterpret_as_uint) or store as Int64/UInt64. % preserves the sign of the left operand; normalize (a - b) mod 256 style expressions into 0..255 before converting to Byte. For byte-codec arithmetic that multiplies by large radices, promote Byte to UInt64; UInt64::to_int() truncates to signed 32 bits for large values.

sh
moon run -c 'fn rotr64(value : UInt64, bits : Int) -> UInt64 { (value >> bits) | (value << (64 - bits)) }
fn main { let value : UInt64 = 0x0123456789abcdef; let full : UInt64 = UInt64::lnot(0); println(rotr64(value, 8).to_string()); println((full + (1 : UInt64)).to_string()); println(((0x80 : UInt64) >> 7).to_string()) }'
# 17222085231038278605
# 0
# 1

UInt64 arithmetic wraps modulo 2^64. Unsigned right shifts are logical. Provide explicit UInt64 context (x : UInt64) when the literal exceeds signed Int range.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
75
Repository forks
11
7-day trend score
22

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/moonbitlang/moonbit-agent-guide/ocaml2moonbit-migration/badge)](https://verified-skill.com/skills/moonbitlang/moonbit-agent-guide/ocaml2moonbit-migration)

Something wrong with this skill or its page? Tell us.