#[cfg(test)]
use kotlin_codegen::KtVis;
use kotlin_codegen::{KtBody, KtCode, KtFun, KtParam, KtType};
use prebindgen_registry::{
expand::{FoldArg, FoldPlan},
Conversions,
};
use super::*;
impl Declarations {
pub(crate) fn validate_split_declarations(
&self,
registry: &Registry<KotlinMeta>,
) -> Result<(), String> {
let type_level = self
.param_expand_decls
.iter()
.map(|d| (d.key().as_str().to_string(), d));
let per_fn = self
.fn_param_expands
.iter()
.map(|(func, param, d)| (format!("fun `{func}` param `{param}`"), d));
for (site, decl) in type_level.chain(per_fn) {
if decl.is_no_split() || decl.variants().len() < 2 {
continue;
}
let target = decl.rust_type().key();
let sigs: Vec<(String, Vec<ErasedJvmType>)> = decl
.variants()
.iter()
.map(|v| {
let ctor = match v {
LocalVariant::Ctor(c) => Some(c),
LocalVariant::SelfIdentity => None,
};
(
ctor.map(|c| c.to_string())
.unwrap_or_else(|| "variant_self()".to_string()),
arm_erased_sig(self, registry, &target, ctor),
)
})
.collect();
for i in 0..sigs.len() {
for j in (i + 1)..sigs.len() {
if sigs[i].1 == sigs[j].1 {
return Err(format!(
"expand_param!({t}) [{site}]: variants {a} and {b} both surface as \
`({sig})` — a split would emit two overloads with the same JVM \
signature; disambiguate the constructors or add .no_split()",
t = decl.key().as_str(),
a = sigs[i].0,
b = sigs[j].0,
sig = sigs[i]
.1
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", "),
));
}
}
}
}
Ok(())
}
}
fn arm_erased_sig(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
target: &TypeKey,
ctor: Option<&syn::Ident>,
) -> Vec<ErasedJvmType> {
match ctor {
Some(cf) => match registry.flat().function(&cf) {
Some(f) => f
.params
.iter()
.map(|p| rust_type_erased(ext, registry, &p.ty))
.collect(),
None => Vec::new(),
},
None => vec![match registry.reading(target) {
Some(reading) => rust_type_erased(ext, registry, &reading),
None => ErasedJvmType::raw(target.as_str().to_string()),
}],
}
}
fn rust_type_erased(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
ty: &prebindgen_registry::flat::TypeRef,
) -> ErasedJvmType {
let peeled = match ty.kind() {
prebindgen_registry::flat::TypeKind::Ref { inner, .. } => inner,
_ => ty,
};
let key = peeled.key();
if ext.types.get(&key).is_some_and(|c| c.name_spec.is_some()) {
if let Some(fqn) = ext.kotlin_fqn(&key) {
return erase_kt_type(&[], &KtType::cls(fqn));
}
}
if let Some(kt) = registry
.input_entry(peeled)
.and_then(|e| e.metadata.kotlin_name.clone())
{
return erase_kt_type(&[], &kt);
}
ErasedJvmType::raw(peeled.key().as_str())
}
fn plan_in_scope(plan: &FoldPlan) -> bool {
plan.selector.is_some()
&& !plan
.variants
.iter()
.any(|v| v.inputs.iter().any(|a| matches!(a, FoldArg::Build(_))))
}
struct Split<'a> {
param: syn::Ident,
plan: &'a FoldPlan,
start: usize,
len: usize,
sel_idx: usize,
optional: bool,
arms: Vec<(usize, Vec<(KtParam, usize)>)>,
}
fn ctor_param_names(f: &prebindgen_registry::flat::Function) -> Vec<String> {
f.params
.iter()
.map(|p| kt_param_name(&p.name.to_string()))
.collect()
}
fn prefixed(origin: &str, name: &str) -> String {
let mut c = name.chars();
match c.next() {
Some(first) => format!("{origin}{}{}", first.to_uppercase(), c.as_str()),
None => origin.to_string(),
}
}
fn non_null(mut ty: KtType) -> KtType {
match &mut ty {
KtType::Named { nullable, .. } | KtType::Function { nullable, .. } => *nullable = false,
}
ty
}
fn variant_typed_params(
registry: &impl Conversions<KotlinMeta>,
variant: &prebindgen_registry::expand::FoldVariant,
origin: &syn::Ident,
block: &[KtParam],
multi: bool,
optional_plan: bool,
) -> Option<Vec<(KtParam, usize)>> {
let origin_kt = kt_param_name(&origin.to_string());
let (names, optional): (Vec<String>, Vec<bool>) = match &variant.ctor {
Some(cf) => {
let f = registry.flat().function(&cf)?;
let optional = f
.params
.iter()
.map(|p| {
matches!(
p.ty.kind(),
prebindgen_registry::flat::TypeKind::Optional(_)
)
})
.collect();
(ctor_param_names(f), optional)
}
None => (vec![origin_kt.clone()], vec![false]),
};
let mut out = Vec::new();
for (m, arg) in variant.inputs.iter().enumerate() {
let FoldArg::Leaf(idx, _) = arg else {
return None;
};
let slot = block.get(*idx)?;
let base = names.get(m).cloned().unwrap_or_else(|| slot.name.clone());
let name = if multi && variant.ctor.is_some() {
prefixed(&origin_kt, &base)
} else {
base
};
let ty = if optional_plan || optional.get(m).copied().unwrap_or(false) {
slot.ty.clone()
} else {
non_null(slot.ty.clone())
};
out.push((KtParam::new(&name, ty), *idx));
}
Some(out)
}
fn find_block(params: &[KtParam], leaf_names: &[String]) -> Option<usize> {
if leaf_names.is_empty() || params.len() < leaf_names.len() {
return None;
}
(0..=params.len() - leaf_names.len()).find(|&s| {
params[s..s + leaf_names.len()]
.iter()
.zip(leaf_names)
.all(|(p, n)| &p.name == n)
})
}
fn resolve_split<'a>(
registry: &'a Registry<KotlinMeta>,
f: &prebindgen_registry::flat::Function,
sel_fun: &KtFun,
param_name: &str,
multi: bool,
) -> Split<'a> {
let param = syn::Ident::new(param_name, Span::call_site());
let plan = registry
.expansion_plans()
.get(&(f.name.clone(), param.clone()))
.unwrap_or_else(|| {
panic!(
"fun!({}).split_on_param(\"{param_name}\"): `{param_name}` is not an expandable \
parameter (it has no `expand_param!` variants)",
f.name
)
});
assert!(
plan.selector.is_some(),
"fun!({}).split_on_param(\"{param_name}\"): `{param_name}` has a single variant — there \
is nothing to split (it already flattens to one signature)",
f.name
);
assert!(
plan_in_scope(plan),
"fun!({}).split_on_param(\"{param_name}\"): `{param_name}` has a recursively-built arm — \
it cannot be overloaded; keep the selector form",
f.name
);
let leaf_names: Vec<String> = plan
.leaves
.iter()
.map(|l| kt_param_name(&l.name.to_string()))
.collect();
let len = leaf_names.len();
let start = find_block(&sel_fun.params, &leaf_names).unwrap_or_else(|| {
panic!(
"fun!({}).split_on_param(\"{param_name}\"): could not locate the parameter's leaf \
block in the generated wrapper",
f.name
)
});
let block = &sel_fun.params[start..start + len];
let sel_idx = plan.selector.expect("selector present");
let optional = plan.produces_option();
let arms: Vec<(usize, Vec<(KtParam, usize)>)> = plan
.variants
.iter()
.enumerate()
.filter(|(_, v)| !optional || v.inputs.len() == 1)
.map(|(vi, v)| {
let typed = variant_typed_params(registry, v, ¶m, block, multi, optional)
.unwrap_or_else(|| {
panic!(
"fun!({}).split_on_param(\"{param_name}\"): an arm has a non-flat input; \
it cannot be overloaded",
f.name
)
});
(vi, typed)
})
.collect();
assert!(
!arms.is_empty(),
"fun!({}).split_on_param(\"{param_name}\"): `{param_name}` is an `Option<_>` parameter \
and none of its arms is a single leaf — its overload has no clean nullable type; keep \
the selector form",
f.name
);
Split {
param,
plan,
start,
len,
sel_idx,
optional,
arms,
}
}
pub(crate) fn render_param_overloads(
ext: &Declarations,
f: &prebindgen_registry::flat::Function,
registry: &Registry<KotlinMeta>,
sel_fun: &KtFun,
) -> Vec<KtFun> {
let requested: Vec<String> = {
let want: std::collections::HashSet<&str> = ext
.fn_split_params
.iter()
.filter(|(func, _)| func == &f.name)
.map(|(_, p)| p.as_str())
.collect();
if want.is_empty() {
return Vec::new();
}
f.params
.iter()
.filter(|p| want.contains(p.name.to_string().as_str()))
.map(|p| p.name.to_string())
.collect()
};
for (func, p) in &ext.fn_split_params {
if func == &f.name && !requested.iter().any(|r| r == p) {
panic!(
"fun!({}).split_on_param(\"{p}\"): no parameter named `{p}` on this function",
f.name
);
}
}
let multi = requested.len() > 1;
let splits: Vec<Split> = requested
.iter()
.map(|name| resolve_split(registry, f, sel_fun, name, multi))
.collect();
let combos = cartesian(&splits.iter().map(|s| s.arms.len()).collect::<Vec<_>>());
let sigs: Vec<Vec<ErasedJvmType>> = combos
.iter()
.map(|combo| {
splits
.iter()
.zip(combo)
.flat_map(|(s, &ai)| {
let ctor = s.plan.variants[s.arms[ai].0].ctor.as_ref();
arm_erased_sig(ext, registry, &s.plan.target.key(), ctor)
})
.collect()
})
.collect();
for i in 0..sigs.len() {
for j in (i + 1)..sigs.len() {
if sigs[i] == sigs[j] {
panic!(
"fun!({}): split_on_param product is ambiguous — combinations {} and {} both \
surface as `({})`; add .no_split() intent is not enough here, disambiguate \
the constructors or drop one .split_on_param",
f.name,
combo_label(&splits, &combos[i]),
combo_label(&splits, &combos[j]),
sigs[i]
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", "),
);
}
}
}
let n = sel_fun.params.len();
let mut out = Vec::with_capacity(combos.len());
for combo in &combos {
let mut params: Vec<KtParam> = Vec::new();
let mut call_args: Vec<String> = Vec::new();
let mut pos = 0usize;
while pos < n {
if let Some((si, s)) = splits.iter().enumerate().find(|(_, s)| s.start == pos) {
let (vi, typed) = &s.arms[combo[si]];
let mut leaf_arg: Vec<String> = vec!["null".to_string(); s.len];
leaf_arg[s.sel_idx] = if s.optional {
format!("if ({} != null) {vi} else -1", typed[0].0.name)
} else {
vi.to_string()
};
for (p, lidx) in typed {
params.push(p.clone());
leaf_arg[*lidx] = p.name.clone();
}
call_args.extend(leaf_arg);
pos += s.len;
} else {
let p = &sel_fun.params[pos];
params.push(p.clone());
call_args.push(p.name.clone());
pos += 1;
}
}
let mut seen = std::collections::HashSet::new();
for p in ¶ms {
assert!(
seen.insert(p.name.clone()),
"fun!({}): split overload has a duplicate parameter name `{}` — rename the \
constructor parameter",
f.name,
p.name
);
}
out.push(overload_shell(
sel_fun,
params,
KtCode::new().line(format!("{}({})", sel_fun.name, call_args.join(", "))),
));
}
out
}
fn overload_shell(sel_fun: &KtFun, params: Vec<KtParam>, body: KtCode) -> KtFun {
let mut ov = sel_fun.clone();
ov.params = params;
ov.kdoc = None;
ov.body = KtBody::Expr(body);
ov
}
fn cartesian(counts: &[usize]) -> Vec<Vec<usize>> {
let mut acc = vec![Vec::new()];
for &c in counts {
acc = acc
.into_iter()
.flat_map(|prefix| {
(0..c).map(move |i| {
let mut next = prefix.clone();
next.push(i);
next
})
})
.collect();
}
acc
}
fn combo_label(splits: &[Split], combo: &[usize]) -> String {
let parts: Vec<String> = splits
.iter()
.zip(combo)
.map(|(s, &ai)| {
let v = match &s.plan.variants[s.arms[ai].0].ctor {
Some(c) => c.to_string(),
None => "variant_self()".to_string(),
};
format!("{}={v}", s.param)
})
.collect();
format!("({})", parts.join(", "))
}
#[cfg(test)]
mod tests {
use prebindgen::SourceLocation;
use super::*;
#[test]
fn split_params_preserve_constructor_option_nullability() {
let ctor: syn::ItemFn = syn::parse_quote! {
pub fn z_summary_optional(count: Option<i64>, total: f64) -> ZSummary {
unimplemented!()
}
};
let registry = crate::test_util::reg_from_items(crate::test_util::declare_referenced(
vec![(syn::Item::Fn(ctor), SourceLocation::default())],
))
.expect("index constructor");
let variant = prebindgen_registry::expand::FoldVariant {
ctor: Some(syn::parse_quote!(z_summary_optional)),
fallible: false,
clone: false,
inputs: vec![FoldArg::Leaf(0, false), FoldArg::Leaf(1, false)],
};
let block = vec![
KtParam::new("expected0", KtType::long().nullable()),
KtParam::new("expected1", KtType::cls("Double").nullable()),
];
let params = variant_typed_params(
®istry,
&variant,
&syn::parse_quote!(expected),
&block,
false,
false,
)
.expect("flat arm");
assert_eq!(params[0].0.ty.to_string(), "Long?");
assert_eq!(params[1].0.ty.to_string(), "Double");
}
#[test]
fn overload_shell_preserves_signature_metadata() {
let sel_fun = KtFun::new("storageSummary")
.vis(KtVis::Public)
.kdoc("Selector-form docs.")
.generic("R")
.annotation("Suppress(\"UNCHECKED_CAST\")")
.modifier("inline")
.param(KtParam::new("sSel", KtType::int()))
.returns(KtType::cls("R"))
.body(KtCode::new().line("TODO()"));
let ov = overload_shell(
&sel_fun,
vec![KtParam::new("count", KtType::long())],
KtCode::new().line("storageSummary(0, count)"),
);
assert_eq!(ov.name, sel_fun.name);
assert_eq!(ov.generics, vec!["R".to_string()]);
assert_eq!(ov.annotations, sel_fun.annotations);
assert_eq!(ov.modifiers, sel_fun.modifiers);
assert!(matches!(ov.vis, KtVis::Public));
assert_eq!(
ov.ret.as_ref().map(|t| t.to_string()),
Some("R".to_string())
);
assert_eq!(ov.kdoc, None);
assert_eq!(ov.params.len(), 1);
assert_eq!(ov.params[0].name, "count");
assert!(matches!(ov.body, KtBody::Expr(_)));
}
}