use kotlin_codegen::KtType;
use prebindgen_registry::flat::TypeRef;
use super::*;
pub(crate) fn enum_probe(reading: &TypeRef) -> &TypeRef {
let mut cur = reading;
while let Some(inner) = cur.borrow_target().or_else(|| cur.optional_inner()) {
cur = inner;
}
cur
}
use prebindgen_registry::shape::fold_shape;
pub(crate) fn handle_kt_type(strategy: &FoldStrategy, leaf: &KtType) -> KtType {
fold_shape(
strategy,
&|| leaf.clone(),
&|inner, _kind, _inner_strategy| inner.nullable(),
&|inner| KtType::generic("List", [inner]),
)
}
pub(crate) fn projection_leaf_kt(ext: &Declarations, proj: &Projection) -> Option<KtType> {
match proj.kind {
ProjectionKind::Handle => ext.kotlin_fqn(&proj.leaf_key).map(KtType::cls),
ProjectionKind::Unsigned64 => Some(KtType::cls("ULong")),
}
}
pub(crate) fn projection_wrap_expr(kind: &ProjectionKind, short: &str, raw: &str) -> String {
match kind {
ProjectionKind::Handle => format!("{short}({raw})"),
ProjectionKind::Unsigned64 => format!("{raw}.toULong()"),
}
}
pub(crate) fn factory_projection_wire_wrap(
proj: &crate::jni::Projection,
short: &str,
name: &str,
) -> (KtType, String) {
use prebindgen_registry::shape::Shape::*;
use crate::jni::{NullableKind, ProjectionKind::*};
let direct = |kind: &crate::jni::ProjectionKind| match kind {
Handle => (KtType::long(), format!("{short}({name})")),
Unsigned64 => (KtType::long(), format!("{name}.toULong()")),
};
match &proj.strategy {
Base => direct(&proj.kind),
Optional(nullable, inner) => {
if !matches!(**inner, Base) {
panic!(
"factory_projection_wire_wrap: only `Nullable<Direct>` projection struct \
fields are supported (field `{name}`)"
);
}
match proj.kind {
Handle => (
KtType::long(),
format!("if ({name} == 0L) null else {short}({name})"),
),
Unsigned64 => match nullable {
NullableKind::Niche => {
let sentinel = projection_leaf_sentinel(proj).unwrap_or_else(|| {
panic!(
"factory_projection_wire_wrap: niche unsigned field `{name}` \
has no declared sentinel"
)
});
(
KtType::long(),
format!("if ({name} == {sentinel}) null else {name}.toULong()"),
)
}
NullableKind::Boxed => {
(KtType::long().nullable(), format!("{name}?.toULong()"))
}
},
}
}
Iterable(_) => panic!(
"factory_projection_wire_wrap: collection (`Vec<projection>`) struct fields are not \
supported by the fromParts factory (field `{name}`)"
),
}
}
pub(crate) fn is_kotlin_primitive_ty(t: &KtType) -> bool {
!t.is_nullable()
&& t.leaf_name().is_some_and(|n| {
matches!(
n,
"Long" | "Int" | "Boolean" | "Double" | "Float" | "Byte" | "Short" | "Char"
)
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn flatten_struct_factory(
ext: &Declarations,
registry: &Registry<KotlinMeta>,
s: &prebindgen_registry::flat::Struct,
prefix: &str,
class_name: &str,
imports: &mut BTreeSet<String>,
depth: usize,
) -> Option<(Vec<(String, KtType)>, String)> {
let plan = build_struct_plan(ext, registry, s, depth)?;
factory_from_plan(&plan, prefix, class_name, imports)
}
fn factory_from_plan(
plan: &StructPlan,
prefix: &str,
class_name: &str,
imports: &mut BTreeSet<String>,
) -> Option<(Vec<(String, KtType)>, String)> {
let mut params: Vec<(String, KtType)> = Vec::new();
let mut parts: Vec<String> = Vec::new();
for f in &plan.fields {
let camel = mangle_kotlin_ident(&kt_snake_to_camel(&f.fname.to_string()));
let base = if prefix.is_empty() {
camel.clone()
} else {
format!("{prefix}_{camel}")
};
let (p, part) = factory_field(&f.kind, &base, imports)?;
params.extend(p);
parts.push(part);
}
let reconstruct = format!("{class_name}({})", parts.join(", "));
Some((params, reconstruct))
}
fn factory_field(
kind: &PlanFieldKind,
base: &str,
imports: &mut BTreeSet<String>,
) -> Option<(Vec<(String, KtType)>, String)> {
let mut params: Vec<(String, KtType)> = Vec::new();
let mut parts: Vec<String> = Vec::new();
let base = base.to_string();
{
let f_kind = kind;
match f_kind {
PlanFieldKind::Projection { proj, fqn, .. } => {
let short = register_fqn(fqn, imports);
let (wire_ty, wrap) = factory_projection_wire_wrap(proj, &short, &base);
params.push((base.clone(), wire_ty));
parts.push(wrap);
}
PlanFieldKind::Enum { kotlin, .. } => {
let short = register_fqn(kotlin.leaf_name()?, imports);
params.push((base.clone(), KtType::int()));
parts.push(format!("{short}.fromInt({base})"));
}
PlanFieldKind::OptionEnum { kotlin, .. } => {
let short = register_fqn(kotlin.leaf_name()?, imports);
params.push((base.clone(), KtType::int().nullable()));
parts.push(format!("{base}?.let {{ {short}.fromInt(it) }}"));
}
PlanFieldKind::Sum {
kotlin_fqn,
optional,
variants,
..
} => {
let iface_short = register_fqn(kotlin_fqn, imports);
let flag = format!("{base}__present");
if *optional {
params.push((flag.clone(), KtType::boolean()));
}
params.push((format!("{base}__tag"), KtType::int()));
let mut arms: Vec<String> = Vec::new();
for (tag, v) in variants.iter().enumerate() {
let mut fwd: Vec<String> = Vec::new();
for pf in &v.fields {
let slot = format!("{base}_{}", pf.slot);
let (group_params, part) = factory_field(&pf.kind, &slot, imports)?;
fwd.push(nullable_group_part(&mut params, group_params, part));
}
let ctor = if v.fields.is_empty() {
format!("{iface_short}.{}", v.kotlin_name)
} else {
format!("{iface_short}.{}({})", v.kotlin_name, fwd.join(", "))
};
arms.push(format!("{tag} -> {ctor}"));
}
let when = format!(
"when ({base}__tag) {{ {}; else -> throw IllegalArgumentException(\"{}: \
invalid tag ${base}__tag\") }}",
arms.join("; "),
iface_short,
);
parts.push(if *optional {
format!("if ({flag}) {when} else null")
} else {
when
});
}
PlanFieldKind::Nested {
optional,
child_fqn,
plan: child,
} => {
let child_fqn = child_fqn.as_ref()?;
let child_short = register_fqn(child_fqn, imports);
let (child_params, _child_reconstruct) =
factory_from_plan(child, &base, &child_short, imports)?;
let child_names = child_params
.iter()
.map(|(n, _)| n.clone())
.collect::<Vec<_>>()
.join(", ");
if !*optional {
params.extend(child_params);
parts.push(format!("{child_short}.fromParts({child_names})"));
} else {
let flag = format!("{base}__present");
let mut fwd_names: Vec<String> = Vec::with_capacity(child_params.len());
params.push((flag.clone(), KtType::boolean()));
for (n, t) in &child_params {
if is_kotlin_primitive_ty(t) || t.is_nullable() {
params.push((n.clone(), t.clone()));
fwd_names.push(n.clone());
} else {
params.push((n.clone(), t.clone().nullable()));
fwd_names.push(format!("{n}!!"));
}
}
parts.push(format!(
"if ({flag}) {child_short}.fromParts({}) else null",
fwd_names.join(", ")
));
}
}
PlanFieldKind::Leaf {
kotlin, nullable, ..
} => {
let ty = if *nullable {
kotlin.clone().nullable()
} else {
kotlin.clone()
};
params.push((base.clone(), ty));
parts.push(base);
}
}
}
debug_assert_eq!(
parts.len(),
1,
"factory_field must yield exactly one reconstruct expression"
);
Some((params, parts.remove(0)))
}
fn nullable_group_part(
params: &mut Vec<(String, KtType)>,
group_params: Vec<(String, KtType)>,
part: String,
) -> String {
let mut part = part;
for (n, t) in group_params {
if is_kotlin_primitive_ty(&t) || t.is_nullable() {
params.push((n, t));
} else {
part = replace_ident(&part, &n, &format!("{n}!!"));
params.push((n, t.nullable()));
}
}
part
}
fn replace_ident(haystack: &str, from: &str, to: &str) -> String {
let is_ident_char = |c: char| c == '_' || c.is_alphanumeric();
let mut out = String::with_capacity(haystack.len());
let mut rest = haystack;
while let Some(pos) = rest.find(from) {
out.push_str(&rest[..pos]);
let before_ok = out.chars().next_back().is_none_or(|c| !is_ident_char(c));
let after = &rest[pos + from.len()..];
let after_ok = after.chars().next().is_none_or(|c| !is_ident_char(c));
if before_ok && after_ok {
out.push_str(to);
} else {
out.push_str(from);
}
rest = after;
}
out.push_str(rest);
out
}
pub(crate) fn render_handle_close(strategy: &crate::jni::FoldStrategy, receiver: &str) -> String {
use prebindgen_registry::shape::Shape::*;
fn go(strategy: &crate::jni::FoldStrategy, receiver: &str, depth: usize) -> String {
match strategy {
Base => format!("{receiver}.close()"),
Optional(_, inner) => match &**inner {
Base => format!("{receiver}?.close()"),
_ => {
let v = format!("e{depth}");
format!("{receiver}?.let {{ {v} -> {} }}", go(inner, &v, depth + 1))
}
},
Iterable(inner) => {
let v = format!("e{depth}");
format!(
"{receiver}.forEach {{ {v} -> {} }}",
go(inner, &v, depth + 1)
)
}
}
}
go(strategy, receiver, 0)
}
pub(crate) fn fold_projection_wrap(
strategy: &crate::jni::FoldStrategy,
receiver: &str,
kind: &crate::jni::ProjectionKind,
wrap_class: &str,
niche_sentinel: Option<&str>,
) -> String {
use prebindgen_registry::shape::Shape::*;
use crate::jni::NullableKind;
fn go(
s: &crate::jni::FoldStrategy,
r: &str,
kind: &crate::jni::ProjectionKind,
w: &str,
sentinel: Option<&str>,
depth: usize,
) -> String {
match s {
Base => projection_wrap_expr(kind, w, r),
Optional(nullable_kind, inner) => match (nullable_kind, &**inner) {
(NullableKind::Niche, Base) if sentinel.is_some() => {
let s = sentinel.unwrap();
let wrapped = projection_wrap_expr(kind, w, "it");
format!("{r}.let {{ if (it == {s}) null else {wrapped} }}")
}
(_, Base) => {
let wrapped = projection_wrap_expr(kind, w, "it");
format!("{r}?.let {{ {wrapped} }}")
}
_ => {
let v = format!("e{depth}");
format!(
"{r}?.let {{ {v} -> {} }}",
go(inner, &v, kind, w, sentinel, depth + 1)
)
}
},
Iterable(inner) => match &**inner {
Base => {
let wrapped = projection_wrap_expr(kind, w, "it");
format!("{r}.map {{ {wrapped} }}")
}
_ => {
let v = format!("e{depth}");
format!(
"{r}.map {{ {v} -> {} }}",
go(inner, &v, kind, w, sentinel, depth + 1)
)
}
},
}
}
go(strategy, receiver, kind, wrap_class, niche_sentinel, 0)
}
pub(crate) fn projection_wire_return(proj: &crate::jni::Projection) -> KtType {
use crate::jni::{FoldStrategy, NullableKind, ProjectionKind};
let (inner_wire, inner_is_primitive) = match proj.kind {
ProjectionKind::Handle => (KtType::long(), true),
ProjectionKind::Unsigned64 => (KtType::long(), true),
};
fold_shape(
&proj.strategy,
&|| inner_wire.clone(),
&|inner, kind, inner_strategy| {
match (kind, inner_strategy) {
(NullableKind::Niche, FoldStrategy::Base) if inner_is_primitive => inner,
_ => inner.nullable(),
}
},
&|inner| KtType::generic("List", [inner]),
)
}
pub(crate) fn projection_leaf_sentinel(proj: &crate::jni::Projection) -> Option<String> {
if let Some(sentinel) = proj.niche_sentinels.first() {
return Some(sentinel.clone());
}
use crate::jni::ProjectionKind;
let leaf_wire: syn::Type = match proj.kind {
ProjectionKind::Handle => syn::parse_quote!(jni::sys::jlong),
ProjectionKind::Unsigned64 => return None,
};
kotlin_null_sentinel(&leaf_wire).map(|s| s.to_string())
}
pub(crate) fn wrap_sentinel(proj: &crate::jni::Projection, nullable: bool) -> Option<String> {
if nullable && matches!(proj.strategy, crate::jni::FoldStrategy::Base) {
return None;
}
projection_leaf_sentinel(proj)
}
pub(crate) fn kotlin_null_sentinel(wire: &syn::Type) -> Option<&'static str> {
let (_, _, is_object) = crate::jni::wire_access::jni_field_access(wire)?;
if is_object {
return None;
}
let syn::Type::Path(tp) = wire else {
return None;
};
let last = tp.path.segments.last()?;
Some(match last.ident.to_string().as_str() {
"jlong" => "0L",
"jint" | "jshort" | "jbyte" | "jchar" => "0",
"jfloat" => "0.0f",
"jdouble" => "0.0",
"jboolean" => "false",
_ => return None,
})
}
pub(crate) fn register_fqn(fqn: &str, used: &mut BTreeSet<String>) -> String {
if fqn.contains('.') {
used.insert(fqn.to_string());
fqn.rsplit('.').next().unwrap_or(fqn).to_string()
} else {
fqn.to_string()
}
}
#[cfg(test)]
mod replace_ident_tests {
use super::replace_ident;
#[test]
fn only_whole_identifiers_are_rewritten() {
assert_eq!(replace_ident("x", "x", "x!!"), "x!!");
assert_eq!(replace_ident("x_2", "x", "x!!"), "x_2");
assert_eq!(replace_ident("ax", "x", "x!!"), "ax");
assert_eq!(
replace_ident("Reading.Exact(exact_v0)", "exact_v0", "exact_v0!!"),
"Reading.Exact(exact_v0!!)"
);
assert_eq!(
replace_ident("f(exact_v0, exact_v01)", "exact_v0", "exact_v0!!"),
"f(exact_v0!!, exact_v01)"
);
}
#[test]
fn a_rejected_match_keeps_its_left_context() {
assert_eq!(replace_ident("axx", "x", "x!!"), "axx");
assert_eq!(replace_ident("xxx", "x", "x!!"), "xxx");
assert_eq!(replace_ident("x ax", "x", "x!!"), "x!! ax");
assert_eq!(replace_ident("x + x", "x", "x!!"), "x!! + x!!");
}
}