# rkyv-mbt codegen

`rkyv-mbt-codegen` generates a typed MoonBit view from the archived layout that
the Rust compiler produces for a `#[derive(Archive)]` named struct or fieldless
enum. It does not
reimplement rkyv's layout rules: the `RkyvMbt` derive emits `size_of` and
`offset_of!` expressions for the concrete `Archived<Type>` generated by rkyv.

The crates are experimental and are not published to crates.io yet. Add them
as Git dependencies from this repository when trying the development version:

```toml
[dependencies]
rkyv = "=0.8.17"
rkyv-mbt-codegen = { git = "https://github.com/mizchi/rkyv-mbt" }
rkyv-mbt-derive = { git = "https://github.com/mizchi/rkyv-mbt" }
```

Its current contract is intentionally narrow:

- rkyv `0.8.17` default format profile only (little-endian, aligned, pointer
  width 32);
- named structs, plus fieldless `#[derive(Archive)]` enums; and
- fields of numeric primitives, `bool`, `String`, `Vec<primitive>`,
  `Vec<String>`, another derive-enabled named struct, or
  `Vec<derive-enabled named struct>`. Any supported non-nested value may be
  wrapped in `Option<T>`, including `Option<Vec<T>>`.

Nested `Option<Option<T>>`, tuple structs, maps, generic types, and enum
variants with payloads are intentionally rejected. Payload enums are supported
through the explicit MoonBit `Schema::TaggedUnion` API: callers must provide
the Rust payload offset and archived size as part of the layout contract.

## Generate a binding

Add the two local crates to the Rust project that owns the archived type, then
make a small generator binary in that same project:

```rust
use rkyv::{Archive, Serialize};
use rkyv_mbt_codegen::write_moonbit;
use rkyv_mbt_derive::RkyvMbt;

#[derive(Archive, Serialize, RkyvMbt)]
pub struct User {
    pub id: u32,
    pub active: bool,
    pub name: String,
    pub scores: Vec<u32>,
}

fn main() -> std::io::Result<()> {
    write_moonbit::<User>("moonbit/generated/user.mbt")
}
```

The generated file assumes this package import:

```moonbit
import {
  "mizchi/rkyv" @rkyv,
}
```

It produces a `UserView::root(bytes)` constructor and typed field methods such
as `user.name()` and `user.scores()`. The root constructor uses the archived
struct size emitted by the Rust compiler; every accessor delegates bounds and
relative-pointer validation to the MoonBit runtime.

`UserView::validate(bytes)` is the untrusted-input entry point. It traverses
all supported generated fields, including every vector element, and checks
canonical bool and `Option` discriminants before returning a `UserView`.

## Generate typed MoonBit input and encoding

Use `render_moonbit_with_encoder()` instead of `write_moonbit` when the
generated package must also create archives accepted by Rust:

```rust
let source = User::rkyv_mbt_schema().render_moonbit_with_encoder()?;
std::fs::write("moonbit/generated/user.mbt", source)?;
```

It adds `UserInput::new(...)` and `input.encode()`. Every supported primitive,
`bool`, `String`, primitive/string/struct vectors, and one-level `Option<T>`
(including `Option<Vec<T>>`) is writable. Types linked through a struct field
must be emitted with the encoder method as well.
The encoder renders `Schema::StructLayout` with the concrete Rust field offsets
and `Archived<T>` size, so it does not infer struct padding from declaration
order.

For nested structs, derive and emit each referenced type into the same MoonBit
package. A generated outer accessor uses the inner view's validated
`InnerView::at(reader, offset)` constructor, so no bytes are copied:

```rust
#[derive(Archive, Serialize, RkyvMbt)]
pub struct Profile {
    pub age: u32,
}

#[derive(Archive, Serialize, RkyvMbt)]
pub struct Account {
    pub id: u32,
    pub profile: Profile,
}

fn main() -> std::io::Result<()> {
    write_moonbit::<Profile>("moonbit/generated/profile.mbt")?;
    write_moonbit::<Account>("moonbit/generated/account.mbt")
}
```

`Vec<Inner>` uses the same pattern lazily. The generated collection view first
validates the complete archived element span using Rust's `size_of` for
`Archived<Inner>`. `at(index)` returns `None` for an out-of-bounds index and
otherwise constructs only that `InnerView`; it does not materialize the vector.

Wrapping any supported field in `Option<T>` produces `T? raise
@rkyv.RkyvError` on the MoonBit side. The derive records the alignment of
`<T as Archive>::Archived`, which lets the runtime compute the rkyv
tag-plus-padding value offset safely.

## Fieldless enums

For a fieldless enum, `RkyvMbt` implements `RkyvMbtEnum` instead of `RkyvMbt`.
Use `rkyv_mbt_enum_schema()` and `render_moonbit_with_encoder()` to emit a
strict `TypeTag` reader plus a public `TypeInput` writer. The generated reader
uses `Reader::read_union_tag`, so an unknown untrusted tag is rejected inside
the runtime rather than attempting to construct its read-only `RkyvError` from
the generated package.

```rust
use rkyv::{Archive, Serialize};
use rkyv_mbt_codegen::RkyvMbtEnum;
use rkyv_mbt_derive::RkyvMbt;

#[derive(Archive, Serialize, RkyvMbt)]
pub enum State {
    Draft,
    Published,
}

let source = State::rkyv_mbt_enum_schema().render_moonbit_with_encoder();
```

Run the codegen tests with:

```sh
just codegen
```
