- name
- ocaml2moonbit-migration
- description
- 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.
# 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 use | MoonBit default |
|---|---|
| binary payload, file contents, compressed/encrypted/checksummed data, parser input | `Bytes` or `BytesView` |
| human-readable text, diagnostics, labels that are truly Unicode | `String` |
| single byte with known 0..255 range | `Byte` |
| indexes, counts, small identifiers, deliberate signed 32-bit wrapping | `Int` |
| file offsets, serialized positions, large object numbers | `Int64` or `UInt64` |
| OCaml `array` with fixed length | `FixedArray[T]` |
| mutable growable builder | `Array[T]` |
| read-only sequence parameter | `ArrayView[T]` or `BytesView` |
| compile-time lookup table (literal or comprehension) | `ReadOnlyArray[T]` |
| keyed lookup with deterministic iteration | `Map[K, V]` |
| OCaml `Buffer.t` byte builder | `@buffer.Buffer` |
| OCaml `ref` | `Ref[T]` |
| OCaml variant | `enum`, often `priv enum` for internal states |
| OCaml record | `struct`, with `{ ..old, field: value }` for immutable update |
| OCaml exception flow | `suberror` plus checked `raise` |
| lazy/deferred state | `Lazy[T]` |
| OCaml `int32` needing wrapping | `Int` |
| OCaml `int32` needing wide arithmetic | `Int64` or `UInt64` |
| OCaml `float` | `Double` |
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)) }
Ver en GitHub