use kotlin_codegen::KtType;
use prebindgen_registry::Conversions;
use super::*;
pub(crate) struct StructPlan {
pub fields: Vec<PlanField>,
}
pub(crate) struct PlanField {
pub fname: syn::Ident,
pub kind: PlanFieldKind,
}
pub(crate) enum LeafForm {
Prim,
IntoObject,
Object,
}
pub(crate) struct ConvChain {
pub stages: Vec<syn::Ident>,
pub function: syn::Ident,
}
impl ConvChain {
fn of(entry: &prebindgen_registry::TypeEntry<KotlinMeta>) -> Self {
ConvChain {
stages: entry
.output_stage_order()
.map(|(_, stage)| stage.function.sig.ident.clone())
.collect(),
function: entry.converter_ident().clone(),
}
}
pub(crate) fn call(&self, env: &TokenStream, value: &TokenStream, base: &str) -> TokenStream {
let function = &self.function;
if self.stages.is_empty() {
return quote! { #function(#env, #value.clone())? };
}
let mut body = TokenStream::new();
let mut previous = quote!(#value.clone());
for (order, stage) in self.stages.iter().enumerate() {
let next = format_ident!("__{}_s{}", base, order);
body.extend(quote! {
let #next = #stage(#env, #previous)
.map_err(|__e| <__JniErr as ::core::convert::From<String>>::from(
__e.to_string()))?;
});
previous = quote!(#next);
}
quote!({ #body #function(#env, #previous)? })
}
}
pub(crate) enum PlanFieldKind {
Projection {
conv: ConvChain,
proj: Projection,
fqn: String,
},
Enum { conv: ConvChain, kotlin: KtType },
OptionEnum { conv: ConvChain, kotlin: KtType },
Nested {
optional: bool,
child_fqn: Option<String>,
plan: StructPlan,
},
Sum {
source: syn::Path,
kotlin_fqn: String,
optional: bool,
variants: Vec<SumPlanVariant>,
},
Leaf {
conv: ConvChain,
wire: Box<syn::Type>,
form: LeafForm,
descriptor: String,
kotlin: KtType,
nullable: bool,
},
}
pub(crate) struct SumPlanVariant {
pub rust_ident: syn::Ident,
pub kotlin_name: String,
pub fields: Vec<SumPlanField>,
}
pub(crate) struct SumPlanField {
pub member: syn::Member,
pub slot: String,
pub kind: PlanFieldKind,
}
pub(crate) fn build_struct_plan(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
s: &prebindgen_registry::flat::Struct,
depth: usize,
) -> Option<StructPlan> {
assert!(
depth <= 16,
"struct fromParts plan: recursion too deep at struct `{}` (cyclic data_class?)",
s.name
);
let mut fields: Vec<PlanField> = Vec::new();
for field in &s.fields {
let fname = field.name.as_ref()?.clone();
let owner = format!("{}.{}", s.name, fname);
let kind = classify_field(ext, registry, &field.ty, &owner, depth)?;
fields.push(PlanField { fname, kind });
}
Some(StructPlan { fields })
}
pub(crate) fn classify_field(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
reading: &prebindgen_registry::flat::TypeRef,
owner: &str,
depth: usize,
) -> Option<PlanFieldKind> {
let optional_inner = reading.optional_inner();
let bare_ref = optional_inner.unwrap_or(reading);
let seq_elem = bare_ref.sequence_elem();
let core = seq_elem.unwrap_or(bare_ref);
if matches!(ext.type_kind(registry, &core.key()), TypeKind::Sum) {
if seq_elem.is_some() {
panic!(
"fromParts bridge: `Vec<{}>` sealed-class field (`{owner}`) is not supported \
(variable arity)",
core,
);
}
return sum_plan_kind(
ext,
registry,
bare_ref,
owner,
optional_inner.is_some(),
depth,
);
}
let field_entry = registry.output_entry(reading)?;
let conv = ConvChain::of(field_entry);
{
if let Some(proj) = field_entry.metadata.projection.clone() {
if matches!(proj.strategy, FoldStrategy::Iterable(_)) {
panic!(
"fromParts bridge: collection (`Vec<projection>`) field `{owner}` is not \
supported — add array codegen to lift this guard"
);
}
let fqn = projection_leaf_kt(ext, &proj)?.to_string();
return Some(PlanFieldKind::Projection { conv, proj, fqn });
}
if ext.is_kotlin_enum_reading(bare_ref) {
return match optional_inner {
None => {
let kotlin = field_entry.metadata.kotlin_name.clone()?;
Some(PlanFieldKind::Enum { conv, kotlin })
}
Some(inner) => {
let kotlin = registry.output_entry(inner)?.metadata.kotlin_name.clone()?;
Some(PlanFieldKind::OptionEnum { conv, kotlin })
}
};
}
let inner_ty = bare_ref;
if let TypeKind::DataStruct { st, cfg } = ext.type_kind(registry, &inner_ty.key()) {
let child_fqn = cfg
.and_then(|c| c.name_spec.as_ref())
.map(|s| ext.fqn_of(s));
let plan = build_struct_plan(ext, registry, st, depth + 1)?;
return Some(PlanFieldKind::Nested {
optional: optional_inner.is_some(),
child_fqn,
plan,
});
}
let wire = field_entry.destination.clone();
let kotlin = field_entry.metadata.kotlin_name.clone()?;
let (form, descriptor) = match jni_field_access(&wire) {
Some((sig, _, false)) => (LeafForm::Prim, sig.to_string()),
Some((sig, _, true)) => (LeafForm::IntoObject, sig.to_string()),
None => {
let slot = optional_inner.unwrap_or(reading);
let descriptor = registry
.output_entry(slot)
.and_then(|e| jni_field_access(&e.destination))
.and_then(|(sig, _, is_obj)| {
if is_obj {
Some(sig.to_string())
} else {
box_descriptor_for_primitive(sig).map(str::to_string)
}
})
.or_else(|| {
match slot.unwrapped().kind() {
prebindgen_registry::flat::TypeKind::Named { id, .. } => id.ident(),
_ => None,
}
.and_then(|name| {
ext.kotlin_fqn(&TypeKey::from_ident(&name))
.map(|v| format!("L{};", v.replace('.', "/")))
})
})
.or_else(|| {
if slot.sequence_elem().is_some() {
Some("Ljava/util/List;".to_string())
} else {
jni_field_access(&wire).map(|(sig, _, _)| sig.to_string())
}
})
.unwrap_or_else(|| "Ljava/lang/Object;".to_string());
(LeafForm::Object, descriptor)
}
};
let nullable = optional_inner.is_some() && !is_jni_primitive(&wire);
Some(PlanFieldKind::Leaf {
conv,
wire: Box::new(wire),
form,
descriptor,
kotlin,
nullable,
})
}
}
impl PlanFieldKind {
pub(crate) fn property_type(&self, owner: &str) -> KtType {
match self {
PlanFieldKind::Projection { proj, fqn, .. } => {
handle_kt_type(&proj.strategy, &KtType::cls(fqn))
}
PlanFieldKind::Enum { kotlin, .. } => kotlin.clone(),
PlanFieldKind::OptionEnum { kotlin, .. } => kotlin.clone().nullable(),
PlanFieldKind::Nested {
optional,
child_fqn,
..
} => {
let fqn = child_fqn.as_ref().unwrap_or_else(|| {
panic!(
"data class property `{owner}`: nested data-class field has no \
registered Kotlin class — declare the child type in a package"
)
});
let t = KtType::cls(fqn);
if *optional {
t.nullable()
} else {
t
}
}
PlanFieldKind::Sum {
kotlin_fqn,
optional,
..
} => {
let t = KtType::cls(kotlin_fqn);
if *optional {
t.nullable()
} else {
t
}
}
PlanFieldKind::Leaf {
kotlin, nullable, ..
} => {
if *nullable {
kotlin.clone().nullable()
} else {
kotlin.clone()
}
}
}
}
pub(crate) fn destructible(&self) -> Option<FoldStrategy> {
match self {
PlanFieldKind::Projection { proj, .. }
if matches!(proj.kind, ProjectionKind::Handle) && proj.owned =>
{
Some(proj.strategy.clone())
}
PlanFieldKind::Sum {
optional, variants, ..
} if variants.iter().any(SumPlanVariant::destructible) => {
Some(whole_value_close(*optional, false))
}
PlanFieldKind::Nested { optional, plan, .. } if plan.destructible() => {
Some(whole_value_close(*optional, false))
}
_ => None,
}
}
}
impl StructPlan {
pub(crate) fn destructible(&self) -> bool {
self.fields.iter().any(|f| f.kind.destructible().is_some())
}
}
impl SumPlanVariant {
pub(crate) fn destructible(&self) -> bool {
self.fields.iter().any(|f| f.kind.destructible().is_some())
}
}
fn whole_value_close(optional: bool, sequence: bool) -> FoldStrategy {
let mut fold = FoldStrategy::Base;
if sequence {
fold = FoldStrategy::Iterable(Box::new(fold));
}
if optional {
fold = FoldStrategy::Optional(NullableKind::Boxed, Box::new(fold));
}
fold
}
pub(crate) fn type_close_strategy(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
ty: &prebindgen_registry::flat::TypeRef,
depth: usize,
) -> Option<FoldStrategy> {
assert!(
depth <= 16,
"close-strategy walk: recursion too deep at type `{}` (cyclic data_class?)",
ty.spell()
);
if let Some(proj) = registry
.output_entry(ty)
.and_then(|e| e.metadata.projection.as_ref())
{
return (matches!(proj.kind, ProjectionKind::Handle) && proj.owned)
.then(|| proj.strategy.clone());
}
let bare = ty.optional_inner().unwrap_or(ty);
let core = bare.sequence_elem().unwrap_or(bare);
let reaches = match ext.type_kind(registry, &core.key()) {
TypeKind::Sum => core
.key()
.ident()
.and_then(|ident| registry.flat().declared_type(&ident))
.is_some_and(|ty| match ty {
prebindgen_registry::flat::Type::Variant(sum) => {
sum.alternatives.iter().any(|alt| {
alt.fields
.iter()
.any(|f| type_close_strategy(ext, registry, &f.ty, depth + 1).is_some())
})
}
_ => false,
}),
TypeKind::DataStruct { st, .. } => st
.fields
.iter()
.any(|f| type_close_strategy(ext, registry, &f.ty, depth + 1).is_some()),
TypeKind::Handle | TypeKind::Enum | TypeKind::Other => false,
};
reaches.then(|| {
whole_value_close(
ty.optional_inner().is_some(),
bare.sequence_elem().is_some(),
)
})
}
fn sum_plan_kind(
ext: &Declarations,
registry: &impl Conversions<KotlinMeta>,
ty: &prebindgen_registry::flat::TypeRef,
owner: &str,
optional: bool,
depth: usize,
) -> Option<PlanFieldKind> {
assert!(
depth <= 16,
"fromParts bridge: sealed-class expansion too deep at `{owner}` (recursive sum?)"
);
let ident = ty.key().ident().unwrap_or_else(|| {
panic!("fromParts bridge: sealed-class field `{owner}` is not a path type")
});
let Some(prebindgen_registry::flat::Type::Variant(sum)) = registry.flat().declared_type(&ident)
else {
panic!("fromParts bridge: sealed-class field `{owner}`: `{ident}` is not an indexed sum")
};
let key = TypeKey::from_ident(&ident);
let cfg = ext
.types
.get(&key)
.unwrap_or_else(|| panic!("fromParts bridge: `{ident}` is not declared"));
let sum_cfg = cfg
.sum()
.unwrap_or_else(|| panic!("fromParts bridge: `{ident}` is not a sealed class"));
let kotlin_fqn = cfg
.name_spec
.as_ref()
.map(|s| ext.fqn_of(s))
.unwrap_or_else(|| panic!("fromParts bridge: sealed class `{ident}` has no Kotlin name"));
let mut variants: Vec<SumPlanVariant> = Vec::new();
for alt in &sum.alternatives {
let kotlin_name = ext.sum_variant_class_name(sum_cfg, &alt.name);
let mut fields: Vec<SumPlanField> = Vec::new();
for field in &alt.fields {
let member = field.member();
let prop = sum_field_prop_name(&member);
let slot = sum_slot_fragment(&kotlin_name, &prop);
let owner = format!("{ident}::{}.{prop}", alt.name);
let kind = classify_field(ext, registry, &field.ty, &owner, depth + 1)?;
fields.push(SumPlanField { member, slot, kind });
}
variants.push(SumPlanVariant {
rust_ident: alt.name.clone(),
kotlin_name,
fields,
});
}
Some(PlanFieldKind::Sum {
source: {
let module = ext.fn_module(registry, &ident);
syn::parse_quote!(#module::#ident)
},
kotlin_fqn,
optional,
variants,
})
}
pub(crate) fn sum_field_prop_name(member: &syn::Member) -> String {
match member {
syn::Member::Named(id) => mangle_kotlin_ident(&kt_snake_to_camel(&id.to_string())),
syn::Member::Unnamed(i) => format!("v{}", i.index),
}
}
pub(crate) fn sum_tag(alt: &prebindgen_registry::flat::Alternative) -> i32 {
alt.index as i32
}
pub(crate) fn sum_slot_fragment(kotlin_variant: &str, prop: &str) -> String {
let mut chars = kotlin_variant.chars();
let head: String = match chars.next() {
Some(c) => c.to_lowercase().collect(),
None => String::new(),
};
format!("{head}{}_{prop}", chars.as_str())
}