///|
/// Emit MoonBit source for derived codecs. The generated code mirrors the
/// hand-written codecs in the desert library: version byte + direct field
/// writes for version 0, the AdtWriter/AdtReader engine for evolved records.
///|
pub fn emit_struct(st : StructIR) -> String {
let sb = StringBuilder()
emit_struct_metadata(st, sb)
if !st.transparent {
emit_struct_serializer(st, sb)
}
emit_struct_deserializer(st, sb)
if st.type_id is Some(id) {
emit_registered_value(st.name, id, sb)
}
sb.to_string()
}
///|
pub fn emit_enum(en : EnumIR) -> String {
let sb = StringBuilder()
for case in en.cases {
if case.steps.length() > 0 {
emit_case_metadata(en.name, case, sb)
}
}
emit_enum_serializer(en, sb)
emit_enum_deserializer(en, sb)
if en.type_id is Some(id) {
emit_registered_value(en.name, id, sb)
}
sb.to_string()
}
///|
fn emit_registered_value(
name : String,
type_id : String,
sb : StringBuilder,
) -> Unit {
sb.write_string("///|\nimpl @desert.RegisteredValue for ")
sb.write_string(name)
sb.write_string(" with registered_type_name(_self) {\n \"")
sb.write_string(type_id)
sb.write_string("\"\n}\n\n///|\nimpl @desert.RegisteredValue for ")
sb.write_string(name)
sb.write_string(
" with serialize_registered(self, ctx) {\n @desert.BinarySerializer::serialize(self, ctx)\n}\n\n",
)
}
///|
/// The metadata value: always emitted so both the V0 reader path and the
/// evolved path share one definition.
fn emit_struct_metadata(st : StructIR, sb : StringBuilder) -> Unit {
sb.write_string("///|\nlet ")
sb.write_string(metadata_ref(st.name))
sb.write_string(
" : @desert.AdtMetadata = @desert.AdtMetadata::new([\n @desert.InitialVersion",
)
for step in st.steps {
sb.write_string(",\n ")
write_evolution_step(step, sb)
}
sb.write_string(",\n])\n\n")
}
///|
fn emit_case_metadata(
type_name : String,
case : CaseIR,
sb : StringBuilder,
) -> Unit {
sb.write_string("///|\nlet ")
sb.write_string(
metadata_ref(type_name) +
"_" +
lowercase_first(case.name) +
"_desert_metadata",
)
sb.write_string(
" : @desert.AdtMetadata = @desert.AdtMetadata::new([\n @desert.InitialVersion",
)
for step in case.steps {
sb.write_string(",\n ")
write_evolution_step(step, sb)
}
sb.write_string(",\n])\n\n")
}
///|
fn write_evolution_step(step : EvolutionIR, sb : StringBuilder) -> Unit {
match step {
FieldAddedIR(name, _default) => {
sb.write_string("@desert.FieldAdded(\"")
sb.write_string(name)
sb.write_string("\")")
}
FieldMadeOptionalIR(name) => {
sb.write_string("@desert.FieldMadeOptional(\"")
sb.write_string(name)
sb.write_string("\")")
}
FieldRemovedIR(name) => {
sb.write_string("@desert.FieldRemoved(\"")
sb.write_string(name)
sb.write_string("\")")
}
FieldMadeTransientIR(name) => {
sb.write_string("@desert.FieldMadeTransient(\"")
sb.write_string(name)
sb.write_string("\")")
}
}
}
///|
fn emit_struct_serializer(st : StructIR, sb : StringBuilder) -> Unit {
sb.write_string("///|\nimpl @desert.BinarySerializer for ")
sb.write_string(st.name)
if st.transparent {
// Transparent: the single field's codec is the whole encoding.
let field = st.fields[0]
sb.write_string(
" with serialize(self, ctx) {\n @desert.BinarySerializer::serialize(self.",
)
sb.write_string(field.name)
sb.write_string(", ctx)\n}\n\n")
} else if st.steps.length() == 0 {
sb.write_string(" with serialize(self, ctx) {\n ctx.write_u8(b'\\x00')\n")
for field in st.fields {
if field.transient_default is Some(_) {
continue
}
sb.write_string(" @desert.BinarySerializer::serialize(self.")
sb.write_string(field.name)
sb.write_string(", ctx)\n")
}
sb.write_string("}\n\n")
} else {
sb.write_string(
" with serialize(self, ctx) {\n let writer = @desert.AdtWriter::new(",
)
sb.write_string(metadata_ref(st.name))
sb.write_string(", ctx)\n")
for field in st.fields {
if field.transient_default is Some(_) {
continue
}
sb.write_string(" writer.write_field(\"")
sb.write_string(field.name)
sb.write_string("\", self.")
sb.write_string(field.name)
sb.write_string(")\n")
}
sb.write_string(" writer.finish()\n}\n\n")
}
}
///|
fn emit_struct_deserializer(st : StructIR, sb : StringBuilder) -> Unit {
sb.write_string("///|\nimpl @desert.BinaryDeserializer for ")
sb.write_string(st.name)
if st.transparent {
let field = st.fields[0]
sb.write_string(" with deserialize(ctx) {\n { ")
sb.write_string(field.name)
sb.write_string(": @desert.BinaryDeserializer::deserialize(ctx) }\n}\n\n")
} else if st.steps.length() == 0 {
sb.write_string(
" with deserialize(ctx) {\n let stored = ctx.read_u8().to_int()\n if stored == 0 {\n {\n",
)
for field in st.fields {
sb.write_string(" ")
sb.write_string(field.name)
sb.write_string(": ")
write_field_read(field, sb)
sb.write_string(",\n")
}
sb.write_string(
" }\n } else {\n let reader = @desert.AdtReader::new(",
)
sb.write_string(metadata_ref(st.name))
sb.write_string(", ctx, stored)\n {\n")
for field in st.fields {
if field.transient_default is Some(default) {
sb.write_string(" ")
sb.write_string(field.name)
sb.write_string(": ")
sb.write_string(default)
sb.write_string(",\n")
continue
}
sb.write_string(" ")
sb.write_string(field.name)
sb.write_string(": ")
write_reader_call(field, sb)
sb.write_string(",\n")
}
sb.write_string(" }\n }\n}\n\n")
} else {
sb.write_string(
" with deserialize(ctx) {\n let stored = ctx.read_u8().to_int()\n let reader = if stored == 0 {\n @desert.AdtReader::new_v0(",
)
sb.write_string(metadata_ref(st.name))
sb.write_string(", ctx)\n } else {\n @desert.AdtReader::new(")
sb.write_string(metadata_ref(st.name))
sb.write_string(", ctx, stored)\n }\n {\n")
for field in st.fields {
if field.transient_default is Some(default) {
sb.write_string(" ")
sb.write_string(field.name)
sb.write_string(": ")
sb.write_string(default)
sb.write_string(",\n")
continue
}
sb.write_string(" ")
sb.write_string(field.name)
sb.write_string(": ")
write_reader_call(field, sb)
sb.write_string(",\n")
}
sb.write_string(" }\n}\n\n")
}
}
///|
fn write_field_read(field : FieldIR, sb : StringBuilder) -> Unit {
if field.transient_default is Some(default) {
sb.write_string(default)
} else if field.is_option {
sb.write_string("@desert.BinaryDeserializer::deserialize(ctx)")
} else {
sb.write_string("@desert.BinaryDeserializer::deserialize(ctx)")
}
}
///|
fn write_reader_call(field : FieldIR, sb : StringBuilder) -> Unit {
if field.is_option {
sb.write_string("reader.read_optional_field(\"")
sb.write_string(field.name)
sb.write_string("\", ")
write_default_arg(field.added_default, true, sb)
sb.write_string(")")
} else {
sb.write_string("reader.read_field(\"")
sb.write_string(field.name)
sb.write_string("\", ")
write_default_arg(field.added_default, false, sb)
sb.write_string(")")
}
}
///|
/// Emit the default argument: `Some(expr)` for read_field, `Some(Some(expr))`
/// for read_optional_field, or plain `None` without a default.
fn write_default_arg(
default : String?,
wrap_option : Bool,
sb : StringBuilder,
) -> Unit {
match default {
Some(expr) => {
sb.write_string("Some(")
if wrap_option {
sb.write_string("Some(")
sb.write_string(expr)
sb.write_string(")")
} else {
sb.write_string(expr)
}
sb.write_string(")")
}
None => sb.write_string("None")
}
}
///|
fn case_ctor_pattern(case : CaseIR, sb : StringBuilder) -> Unit {
sb.write_string(case.name)
if case.arity > 0 {
sb.write_string("(")
for i in 0.. 0 {
sb.write_string(", ")
}
sb.write_string("f")
sb.write_string(i.to_string())
}
sb.write_string(")")
}
}
///|
/// Pattern for the constructor-index match: payloads are not inspected.
fn case_ctor_pattern_wildcard(case : CaseIR, sb : StringBuilder) -> Unit {
sb.write_string(case.name)
if case.arity > 0 {
sb.write_string("(")
for i in 0.. 0 {
sb.write_string(", ")
}
sb.write_string("_")
}
sb.write_string(")")
}
}
///|
fn emit_enum_serializer(en : EnumIR, sb : StringBuilder) -> Unit {
sb.write_string("///|\nimpl @desert.BinarySerializer for ")
sb.write_string(en.name)
sb.write_string(
" with serialize(self, ctx) {\n ctx.write_u8(b'\\x00')\n let idx : Int32 = match self {\n",
)
let mut idx = 0
for case in en.cases {
if case.transient {
continue
}
sb.write_string(" ")
case_ctor_pattern_wildcard(case, sb)
sb.write_string(" => ")
sb.write_string(idx.to_string())
sb.write_string("\n")
idx = idx + 1
}
for case in en.cases {
if case.transient {
sb.write_string(" ")
case_ctor_pattern_wildcard(case, sb)
sb.write_string(" => raise @desert.SerializingTransientConstructor(\"")
sb.write_string(en.name)
sb.write_string(".")
sb.write_string(case.name)
sb.write_string("\")\n")
}
}
sb.write_string(
" }\n ctx.write_var_u32(idx.reinterpret_as_uint())\n match self {\n",
)
for case in en.cases {
if case.transient {
sb.write_string(" ")
case_ctor_pattern_wildcard(case, sb)
sb.write_string(" => raise @desert.SerializingTransientConstructor(\"")
sb.write_string(en.name)
sb.write_string(".")
sb.write_string(case.name)
sb.write_string("\")\n")
continue
}
sb.write_string(" ")
case_ctor_pattern(case, sb)
sb.write_string(" => {\n")
if case.transparent {
if case.arity == 0 {
// Transparent unit case: the constructor index is the entire payload.
} else {
// Payload is the single inner value, no case version byte.
sb.write_string(" @desert.BinarySerializer::serialize(f0, ctx)\n")
}
} else if case.steps.length() == 0 {
sb.write_string(" ctx.write_u8(b'\\x00')\n")
for i in 0.. Unit {
sb.write_string("///|\nimpl @desert.BinaryDeserializer for ")
sb.write_string(en.name)
sb.write_string(
" with deserialize(ctx) {\n let enum_stored = ctx.read_u8().to_int()\n if enum_stored != 0 {\n raise @desert.DeserializationFailure(\"Enum \\\"",
)
sb.write_string(en.name)
sb.write_string(
"\\\" was written with container evolution, which this codec does not support\")\n }\n let idx = ctx.read_var_u32().reinterpret_as_int()\n match idx {\n",
)
let mut idx = 0
for case in en.cases {
if case.transient {
continue
}
sb.write_string(" ")
sb.write_string(idx.to_string())
sb.write_string(" => {\n")
if case.transparent {
sb.write_string(" ")
sb.write_string(case.name)
if case.arity > 0 {
sb.write_string("(@desert.BinaryDeserializer::deserialize(ctx))")
}
sb.write_string("\n")
} else if case.steps.length() == 0 {
sb.write_string(
" let case_stored = ctx.read_u8().to_int()\n if case_stored != 0 {\n raise @desert.DeserializationFailure(\"Case \\\"",
)
sb.write_string(case.name)
sb.write_string(
"\\\" was written with evolution steps, which this codec does not support\")\n }\n ",
)
sb.write_string(case.name)
if case.arity > 0 {
sb.write_string("(")
for i in 0.. 0 {
sb.write_string(", ")
}
sb.write_string("@desert.BinaryDeserializer::deserialize(ctx)")
}
sb.write_string(")")
}
sb.write_string("\n")
} else {
sb.write_string(
" let case_stored = ctx.read_u8().to_int()\n let reader = if case_stored == 0 {\n @desert.AdtReader::new_v0(",
)
sb.write_string(
metadata_ref(en.name) +
"_" +
lowercase_first(case.name) +
"_desert_metadata",
)
sb.write_string(", ctx)\n } else {\n @desert.AdtReader::new(")
sb.write_string(
metadata_ref(en.name) +
"_" +
lowercase_first(case.name) +
"_desert_metadata",
)
sb.write_string(", ctx, case_stored)\n }\n ")
sb.write_string(case.name)
if case.arity > 0 {
sb.write_string("(")
for i in 0.. 0 {
sb.write_string(", ")
}
if case.option_flags[i] {
sb.write_string("reader.read_optional_field(\"field")
sb.write_string(i.to_string())
sb.write_string("\", None)")
} else {
sb.write_string("reader.read_field(\"field")
sb.write_string(i.to_string())
sb.write_string("\", None)")
}
}
sb.write_string(")")
}
sb.write_string("\n")
}
sb.write_string(" }\n")
idx = idx + 1
}
sb.write_string(" other => raise @desert.InvalidConstructorId(other, \"")
sb.write_string(en.name)
sb.write_string("\")\n }\n}\n\n")
}
///|
/// Top-level `let` names must start lowercase.
pub fn lowercase_first(s : String) -> String {
if s.is_empty() {
return s
}
let first = match s.get_char(0) {
Some(c) => c
None => return s
}
let lowered = match first.to_int() {
cp if cp >= 'A'.to_int() && cp <= 'Z'.to_int() => (cp + 32).unsafe_to_char()
_ => first
}
let sb = StringBuilder()
sb.write_char(lowered)
for i in 1.. sb.write_char(c)
None => ()
}
}
sb.to_string()
}
///|
pub fn metadata_ref(type_name : String) -> String {
lowercase_first(type_name) + "_desert_metadata"
}