// Rendering `Target`s as MoonBit source.
//
// Two naming conventions keep generated code from colliding with the user's
// own names: the trait method parameters are `serializer` / `deserializer`
// rather than `s` / `d`, and accumulators are prefixed `f_`. Labelled variant
// payloads are bound with explicit `label=binding` patterns for the same
// reason — a variant field called `deserializer` would otherwise shadow it.
///|
/// The banner every generated file opens with.
const HEADER : String = "// Generated by `moonx Yu-zh/data/derive`. DO NOT EDIT.\n// Re-run the generator after changing the types it was generated from."
///|
fn Target::self_type(self : Target) -> String {
if self.params.is_empty() {
self.name
} else {
"\{self.name}[\{self.params.join(", ")}]"
}
}
///|
/// `pub impl[T : @data.Serialize] @data.Serialize for Foo[T]`
fn Target::impl_head(self : Target, trait_ : String) -> String {
let visibility = if self.public { "pub " } else { "" }
let bounds = if self.params.is_empty() {
""
} else {
let each = self.params.map(p => "\{p} : @data.\{trait_}")
"[\{each.join(", ")}]"
}
"\{visibility}impl\{bounds} @data.\{trait_} for \{self.self_type()}"
}
///|
/// Whether the generated `serialize` body reads the value at all.
///
/// A type with nothing left to write — no fields, or none that survive
/// `#data.skip` — hands its parameter to no one, and an unused binding named
/// `self` would warn. Generated code lands in projects that have no way to
/// annotate away a warning they did not write, so the binder is named for what
/// the body does with it.
fn Target::reads_self(self : Target) -> Bool {
match self.shape {
UnitStruct => false
NewtypeStruct | Variant(_) => true
Record(fields) => !fields.filter(f => !f.skip).is_empty()
TupleStruct(arity) => arity > 0
}
}
///|
fn quote(text : String) -> String {
let out = StringBuilder::new()
out.write_char('"')
for c in text {
match c {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
_ => out.write_char(c)
}
}
out.write_char('"')
out.to_string()
}
///|
fn hex_digit(n : Int) -> Char {
if n < 10 {
('0'.to_int() + n).unsafe_to_char()
} else {
('a'.to_int() + n - 10).unsafe_to_char()
}
}
///|
/// Renders a UTF-8 byte literal without relying on the generated source file's
/// text encoding. Printable ASCII stays readable; everything else uses `\xNN`.
fn byte_quote(text : String) -> String {
let out = StringBuilder::new()
out.write_string("b\"")
for byte in @utf8.encode(text) {
let n = byte.to_int()
match n {
0x22 => out.write_string("\\\"")
0x5C => out.write_string("\\\\")
_ =>
if n >= 0x20 && n <= 0x7E {
out.write_char(n.unsafe_to_char())
} else {
out.write_string("\\x")
out.write_char(hex_digit(n >> 4))
out.write_char(hex_digit(n & 0x0F))
}
}
}
out.write_char('"')
out.to_string()
}
///|
fn byte_pattern(text : String) -> String {
let out = StringBuilder::new()
out.write_char('[')
for index, byte in @utf8.encode(text) {
if index > 0 {
out.write_string(", ")
}
let n = byte.to_int()
out.write_string("0x")
out.write_char(hex_digit(n >> 4))
out.write_char(hex_digit(n & 0x0F))
}
out.write_char(']')
out.to_string()
}
///|
fn name_list(fields : Array[Field]) -> String {
let names = fields.filter(f => !f.skip).map(f => quote(f.name))
"[\{names.join(", ")}]"
}
///|
fn record_field_names_name(target : Target) -> String {
"__data_record_fields_\{target.name}"
}
///|
fn variant_field_names_name(target : Target, index : Int) -> String {
"__data_variant_fields_\{target.name}_\{index}"
}
///|
fn variant_names_name(target : Target) -> String {
"__data_variants_\{target.name}"
}
///|
fn record_field_identifier_name(target : Target) -> String {
"DataRecordField\{target.name}"
}
///|
fn variant_field_identifier_name(target : Target, index : Int) -> String {
"DataVariantField\{target.name}\{index}"
}
///|
fn field_case(field : Field) -> String {
"Field_\{field.ident}"
}
///|
fn unknown_field_case() -> String {
"Unknown"
}
///|
/// Emits the generated equivalent of Serde's private `__Field` enum and its
/// visitor. The declaration-order hint makes the common path one comparison;
/// the fallback is an exact `BytesView` pattern match, giving MoonBit's match
/// compiler the complete field set from which to build a decision tree.
fn write_field_identifier(
out : StringBuilder,
identifier : String,
fields : Array[Field],
) -> Unit {
let kept = fields.filter(field => !field.skip)
let unknown = unknown_field_case()
out.write_string("\n///|\n")
out.write_string("priv enum \{identifier} {\n")
for field in kept {
out.write_string(" \{field_case(field)}\n")
}
out.write_string(" Unknown\n")
out.write_string("}\n")
out.write_string("\n///|\n")
if kept.is_empty() {
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_index(_index) {\n \{unknown}\n}\n",
)
out.write_string("\n///|\n")
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_bytes(_key, _index_hint) {\n \{unknown}\n}\n",
)
out.write_string("\n///|\n")
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_string(_name) {\n \{unknown}\n}\n",
)
} else {
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_index(index) {\n match index {\n",
)
for index, field in kept {
out.write_string(" \{index} => \{field_case(field)}\n")
}
out.write_string(" _ => \{unknown}\n }\n}\n")
out.write_string("\n///|\n")
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_bytes(key, index_hint) {\n match index_hint {\n",
)
for index, field in kept {
out.write_string(
" \{index} if key.equal_to_bytes(\{byte_quote(field.name)}) => \{field_case(field)}\n",
)
}
out.write_string(" _ =>\n match key {\n")
for field in kept {
out.write_string(
" \{byte_pattern(field.name)} => \{field_case(field)}\n",
)
}
out.write_string(" _ => \{unknown}\n }\n }\n}\n")
out.write_string("\n///|\n")
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn from_string(name) {\n match name {\n",
)
for field in kept {
out.write_string(" \{quote(field.name)} => \{field_case(field)}\n")
}
out.write_string(" _ => \{unknown}\n }\n}\n")
}
out.write_string("\n///|\n")
out.write_string(
"impl @data.FieldIdentifier for \{identifier} with fn name(self) {\n match self {\n",
)
for field in kept {
out.write_string(" \{field_case(field)} => Some(\{quote(field.name)})\n")
}
out.write_string(" \{unknown} => None\n }\n}\n")
}
///|
fn write_field_metadata(
out : StringBuilder,
binding : String,
identifier : String,
fields : Array[Field],
) -> Unit {
out.write_string("\n///|\n")
out.write_string(
"let \{binding} : ReadOnlyArray[String] = \{name_list(fields)}\n",
)
write_field_identifier(out, identifier, fields)
}
///|
fn write_all_field_metadata(out : StringBuilder, target : Target) -> Unit {
match target.shape {
Record(fields) =>
write_field_metadata(
out,
record_field_names_name(target),
record_field_identifier_name(target),
fields,
)
Variant(cases) => {
let names = cases.map(c => quote(c.name))
out.write_string("\n///|\n")
out.write_string(
"let \{variant_names_name(target)} : ReadOnlyArray[String] = [\{names.join(", ")}]\n",
)
for index, case_ in cases {
match case_.payload {
Struct(fields) =>
write_field_metadata(
out,
variant_field_names_name(target, index),
variant_field_identifier_name(target, index),
fields,
)
_ => ()
}
}
}
_ => ()
}
}
///|
fn variant_info(target : Target, index : Int, case_ : Case) -> String {
"@data.VariantInfo::{ enum_name: \{quote(target.name)}, index: \{index}, name: \{quote(case_.name)} }"
}
///|
/// Writes the body that reads a set of named fields into `f_*` accumulators.
///
/// Shared by structs and struct-shaped enum variants, which differ only in
/// what they build at the end.
fn write_field_loop(
out : StringBuilder,
fields : Array[Field],
field_identifier : String,
indent : String,
) -> Unit {
for field in fields {
if !field.skip {
out.write_string("\{indent}let mut f_\{field.ident} = None\n")
}
}
out.write_string(
"\{indent}while (deserializer.deserialize_field() : \{field_identifier}?) is Some(field) {\n",
)
out.write_string("\{indent} match field {\n")
for field in fields {
if field.skip {
continue
}
// An `Option` field takes the read straight, so an absent field and an
// explicit null both land on `None`. Everything else is wrapped so that
// absence stays distinguishable and can be reported by name.
let rhs = if field.optional {
"deserializer.deserialize_field_value()"
} else {
"Some(deserializer.deserialize_field_value())"
}
out.write_string(
"\{indent} \{field_case(field)} => f_\{field.ident} = \{rhs}\n",
)
}
out.write_string(
"\{indent} \{unknown_field_case()} => deserializer.skip_value()\n",
)
out.write_string("\{indent} }\n")
out.write_string("\{indent}}\n")
}
///|
fn field_value(field : Field, indent : String) -> String {
if field.skip {
"Default::default()"
} else if field.optional {
"f_\{field.ident}"
} else {
"match f_\{field.ident} {\n\{indent} Some(value) => value\n\{indent} None => @data.required(None, name=\{quote(field.name)}, path=deserializer.path())\n\{indent}}"
}
}
///|
/// The `extend` declaration for the method an impl silently promotes.
///
/// A trait impl installs its methods as regular methods on the type as well,
/// so a derived `Serialize` would quietly add `value.serialize(s)` to the
/// public surface of a project that only asked to derive. Declaring it
/// deprecated and hidden keeps that out of the user's interface while leaving
/// the promotion legal for anyone already relying on it: the trait bound is
/// how generated code reaches these, and how everyone else should.
fn write_extend(out : StringBuilder, target : Target, trait_ : String) -> Unit {
let visibility = if target.public { "pub " } else { "" }
let method_name = if trait_ == "Serialize" {
"serialize"
} else {
"deserialize"
}
out.write_string("\n///|\n")
out.write_string(
"#deprecated(\"reach it through the `\{trait_}` bound\", skip_current_package=true)\n",
)
out.write_string("#doc(hidden)\n")
out.write_string(
"\{visibility}extend \{target.name} with @data.\{trait_}::{\{method_name}}\n",
)
}
///|
fn write_serialize(out : StringBuilder, target : Target) -> Unit {
let self_ = if target.reads_self() { "self" } else { "_self" }
out.write_string("\n///|\n")
out.write_string(
"\{target.impl_head("Serialize")} with fn serialize(\{self_}, serializer) {\n",
)
match target.shape {
UnitStruct =>
out.write_string(
" serializer.serialize_unit_struct(\{quote(target.name)})\n",
)
NewtypeStruct =>
out.write_string(
" serializer.serialize_newtype_struct(\{quote(target.name)}, self.0)\n",
)
Record(fields) => {
let kept = fields.filter(f => !f.skip)
out.write_string(
" serializer.serialize_struct_begin(\{quote(target.name)}, \{kept.length()})\n",
)
for field in kept {
out.write_string(
" serializer.serialize_field(\{quote(field.name)}, self.\{field.ident})\n",
)
}
out.write_string(" serializer.serialize_struct_end()\n")
}
TupleStruct(arity) => {
out.write_string(
" serializer.serialize_tuple_struct_begin(\{quote(target.name)}, \{arity})\n",
)
for i in 0.. {
out.write_string(" match self {\n")
for index, case_ in cases {
let info = variant_info(target, index, case_)
match case_.payload {
Unit =>
out.write_string(
" \{case_.ident} => serializer.serialize_unit_variant(\{info})\n",
)
Newtype =>
out.write_string(
" \{case_.ident}(v0) => serializer.serialize_newtype_variant(\{info}, v0)\n",
)
Tuple(arity) => {
let binds = Array::makei(arity, i => "v\{i}")
out.write_string(" \{case_.ident}(\{binds.join(", ")}) => {\n")
out.write_string(
" serializer.serialize_tuple_variant_begin(\{info}, \{arity})\n",
)
for bind in binds {
out.write_string(
" serializer.serialize_tuple_variant_element(\{bind})\n",
)
}
out.write_string(" serializer.serialize_tuple_variant_end()\n")
out.write_string(" }\n")
}
Struct(fields) => {
let binds = fields.mapi((i, f) => "\{f.ident}=v\{i}")
out.write_string(" \{case_.ident}(\{binds.join(", ")}) => {\n")
out.write_string(
" serializer.serialize_struct_variant_begin(\{info}, \{fields.length()})\n",
)
for i, field in fields {
out.write_string(
" serializer.serialize_struct_variant_field(\{quote(field.name)}, v\{i})\n",
)
}
out.write_string(
" serializer.serialize_struct_variant_end()\n",
)
out.write_string(" }\n")
}
}
}
out.write_string(" }\n")
}
}
out.write_string("}\n")
}
///|
fn write_deserialize(out : StringBuilder, target : Target) -> Unit {
out.write_string("\n///|\n")
out.write_string(
"\{target.impl_head("Deserialize")} with fn deserialize(deserializer) {\n",
)
match target.shape {
UnitStruct => {
out.write_string(
" deserializer.deserialize_unit_struct(\{quote(target.name)})\n",
)
// Spelled with the type name rather than as a bare `{ }`, which has no
// fields to infer the type from.
out.write_string(" \{target.name}::{ }\n")
}
NewtypeStruct =>
out.write_string(
" \{target.name}(deserializer.deserialize_newtype_struct(\{quote(target.name)}))\n",
)
Record(fields) => {
let field_names = record_field_names_name(target)
let field_identifier = record_field_identifier_name(target)
out.write_string(
" deserializer.deserialize_struct_begin(\{quote(target.name)}, \{field_names})\n",
)
write_field_loop(out, fields, field_identifier, " ")
out.write_string(" {\n")
for field in fields {
out.write_string(" \{field.ident}: \{field_value(field, " ")},\n")
}
out.write_string(" }\n")
}
TupleStruct(arity) => {
out.write_string(
" deserializer.deserialize_tuple_struct_begin(\{quote(target.name)}, \{arity})\n",
)
for i in 0.. "v\{i}")
out.write_string(" \{target.name}(\{binds.join(", ")})\n")
}
Variant(cases) => {
let variants = variant_names_name(target)
out.write_string(
" let value = match deserializer.deserialize_enum_begin(\{quote(target.name)}, \{variants}) {\n",
)
for i, case_ in cases {
match case_.payload {
Unit => {
out.write_string(" \{i} => {\n")
out.write_string(" deserializer.deserialize_unit_variant()\n")
out.write_string(" \{case_.ident}\n")
out.write_string(" }\n")
}
Newtype =>
out.write_string(
" \{i} => \{case_.ident}(deserializer.deserialize_newtype_variant())\n",
)
Tuple(arity) => {
out.write_string(" \{i} => {\n")
out.write_string(
" deserializer.deserialize_tuple_begin(\{arity})\n",
)
for i in 0.. "v\{i}")
out.write_string(" \{case_.ident}(\{binds.join(", ")})\n")
out.write_string(" }\n")
}
Struct(fields) => {
let field_names = variant_field_names_name(target, i)
let field_identifier = variant_field_identifier_name(target, i)
out.write_string(" \{i} => {\n")
out.write_string(
" deserializer.deserialize_struct_begin(\{quote(case_.name)}, \{field_names})\n",
)
write_field_loop(out, fields, field_identifier, " ")
out.write_string(" \{case_.ident}(\n")
for field in fields {
out.write_string(
" \{field.ident}=\{field_value(field, " ")},\n",
)
}
out.write_string(" )\n")
out.write_string(" }\n")
}
}
}
// Unreachable: `deserialize_enum_begin` is contractually required to
// raise rather than return an index outside `variants`. The arm exists
// for exhaustiveness, and reports a format that broke that contract
// rather than trusting it.
out.write_string(
" index =>\n raise @data.UnknownVariant(path=deserializer.path(), variant=index.to_string(), expected=\{variants})\n",
)
out.write_string(" }\n")
out.write_string(" deserializer.deserialize_enum_end()\n")
out.write_string(" value\n")
}
}
out.write_string("}\n")
}
///|
/// Renders every impl for one file's worth of targets, or `None` when nothing
/// in the file was marked.
fn emit(targets : Array[Target]) -> String? {
let wanted = targets.filter(t => t.serialize || t.deserialize)
if wanted.is_empty() {
return None
}
let out = StringBuilder::new()
out.write_string(HEADER)
out.write_string("\n")
for target in wanted {
if target.serialize {
write_serialize(out, target)
write_extend(out, target, "Serialize")
}
if target.deserialize {
write_all_field_metadata(out, target)
write_deserialize(out, target)
write_extend(out, target, "Deserialize")
}
}
Some(out.to_string())
}