///|
/// Scan parsed sources for `#derive.binary_codec` targets and build the IR.
///|
fn is_enum(desc : @syntax.TypeDesc) -> Bool {
match desc {
@syntax.Variant(_) | @syntax.ExtensibleEnum(_) => true
_ => false
}
}
///|
fn is_option_type(ty : @syntax.Type) -> Bool {
match ty {
@syntax.Option(..) => true
@syntax.Name(constr_id~, tys~, ..) =>
match constr_id.id {
@syntax.Ident(name~) => name == "Option" && tys.length() == 1
@syntax.Dot(pkg~, id~) =>
(pkg == "core" || pkg == "") && id == "Option" && tys.length() == 1
}
_ => false
}
}
///|
fn field_transient_default(
attrs : @list.List[@attribute.Attribute],
) -> String? raise CodegenError {
for attr in attrs.to_array() {
if attr.raw == "#transient" {
// `#transient` with no argument is not meaningful for serialization;
// treat it as a null-ish default placeholder.
return Some("None")
}
if attr.raw.has_prefix("#transient(") {
let inner = parse_between_parens_public(attr.raw)
return Some(inner)
}
}
None
}
///|
fn parse_struct(
name : String,
desc : @syntax.TypeDesc,
options : DeriveOptions,
) -> StructIR raise CodegenError {
match desc {
@syntax.Record(fields~, ..) => {
let field_irs : Array[FieldIR] = []
for field in fields.to_array() {
field_irs.push({
name: field.name.label,
is_option: is_option_type(field.ty),
transient_default: field_transient_default(field.attrs),
added_default: added_default_of(options.steps, field.name.label),
})
}
if options.transparent && field_irs.length() != 1 {
raise CodegenError("transparent requires exactly one field: \{name}")
}
{
name,
fields: field_irs,
steps: options.steps,
transparent: options.transparent,
type_id: options.type_id,
}
}
@syntax.TupleStruct(_) =>
raise CodegenError(
"tuple structs are not supported by desert-codegen: \{name}",
)
_ =>
raise CodegenError(
"expected a struct for #derive.binary_codec, got another kind: \{name}",
)
}
}
///|
fn parse_enum(
name : String,
constrs : @list.List[@syntax.ConstrDecl],
options : DeriveOptions,
) -> EnumIR raise CodegenError {
let cases : Array[CaseIR] = []
for constr in constrs.to_array() {
let case_transient = has_raw_attr(constr.attrs, "#desert(transient)")
let case_steps : Array[EvolutionIR] = []
let mut case_transparent = false
for attr in constr.attrs.to_array() {
if attr.raw == "#desert(transparent)" {
case_transparent = true
} else if attr.raw.has_prefix("#desert(evolution(") {
let body = parse_between_parens_public(attr.raw)
for step in split_top_level(body) {
case_steps.push(parse_evolution_step(step))
}
} else if attr.raw == "#desert" || attr.raw.has_prefix("#desert(") {
// Unknown #desert directive on a case: ignore.
}
}
let arity = match constr.args {
Some(args) => args.length()
None => 0
}
let option_flags : Array[Bool] = []
match constr.args {
Some(args) =>
for arg in args {
option_flags.push(is_option_type(arg.ty))
}
None => ()
}
if case_transparent && arity != 1 {
raise CodegenError(
"transparent case must have exactly one field: \{name}.\{constr.name.name}",
)
}
cases.push({
name: constr.name.name,
arity,
option_flags,
steps: case_steps,
transient: case_transient,
transparent: case_transparent,
})
}
if options.transparent {
raise CodegenError("transparent is not supported on enums: \{name}")
}
let ordered_cases = if options.sorted_constructors {
sort_cases_by_name(cases)
} else {
cases
}
{
name,
cases: ordered_cases,
sorted_constructors: options.sorted_constructors,
type_id: options.type_id,
}
}
///|
/// The default expression declared by a FieldAdded step for `field_name`.
fn added_default_of(steps : Array[EvolutionIR], field_name : String) -> String? {
for step in steps {
match step {
FieldAddedIR(name, default) =>
if name == field_name {
return Some(default)
}
_ => ()
}
}
None
}
///|
fn has_raw_attr(attrs : @list.List[@attribute.Attribute], raw : String) -> Bool {
for attr in attrs.to_array() {
if attr.raw == raw {
return true
}
}
false
}
///|
fn sort_cases_by_name(cases : Array[CaseIR]) -> Array[CaseIR] {
// Stable insertion sort by case name.
let sorted : Array[CaseIR] = []
for c in cases {
let mut inserted = false
for i in 0.. String raise CodegenError {
let start = index_of_char(s, '(')
if start < 0 {
raise CodegenError("no parentheses in: \{s}")
}
match matching_paren(s, start) {
Some(end) => s[start + 1:end].to_owned()
None => raise CodegenError("unbalanced parentheses in: \{s}")
}
}
///|
/// Walk one parsed source file, returning the derived structs and enums.
pub fn scan_impls(
impls : @list.List[@syntax.Impl],
) -> (Array[StructIR], Array[EnumIR]) raise CodegenError {
let structs : Array[StructIR] = []
let enums : Array[EnumIR] = []
for impl_ in impls.to_array() {
match impl_ {
@syntax.TopTypeDef(type_decl) => {
let mut found : DeriveOptions? = None
for attr in type_decl.attrs.to_array() {
if is_binary_codec_attr(attr.raw) {
let body = if attr.raw.has_prefix("#desert(binary_codec") {
parse_between_parens_public(attr.raw)
} else {
""
}
found = Some(parse_derive_args(body))
break
}
}
match found {
Some(options) => {
let name = type_decl.tycon
if is_enum(type_decl.components) && options.steps.length() > 0 {
raise CodegenError(
"container-level evolution on enums is not supported (put evolution steps on individual cases instead): \{name}",
)
}
match type_decl.components {
@syntax.Record(..) =>
structs.push(parse_struct(name, type_decl.components, options))
@syntax.Variant(constrs) =>
enums.push(parse_enum(name, constrs, options))
@syntax.ExtensibleEnum(Some(constrs)) =>
enums.push(parse_enum(name, constrs, options))
_ =>
raise CodegenError(
"#derive.binary_codec requires a struct or enum: \{name}",
)
}
}
None => ()
}
}
_ => ()
}
}
(structs, enums)
}