1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//! `JniGen::report()` — the resolved binding surface,
//! explained.
//!
//! Declarations act at a distance: one `expand_return!` / `convert!` /
//! error decl reshapes every function touching its type, and the effect
//! set is computed inside `resolve`. The report is the missing *explain*
//! mode: for each declared function its FINAL Kotlin signature (rendered
//! through the same [`render_wrapper_fn`] the emitters use, so it cannot
//! drift from the real output) plus the plans that shaped it, and for
//! each type its kind / Kotlin FQN / wire. Consumers write it next to the
//! committed regen so a decl's effect is reviewable in a PR without
//! reading generated Kotlin.
//!
//! The report is deliberately an inherent method of `JniGen`
//! (the `write_kotlin` seam): the *pattern* — describe your resolved
//! surface — is adapter-universal, but the *content* is intrinsically in
//! the destination language's vocabulary, so each adapter implements its
//! own.
use kotlin_codegen::KtFun;
use prebindgen_registry::Conversions;
use super::*;
impl super::JniGen {
/// Render the resolved binding surface as a deterministic markdown
/// report: per package / class the final Kotlin signature of every
/// wrapper (exactly as generated) with the expand/error plans that
/// shaped it, then the type table (kind, Kotlin FQN, wire, conversion
/// sources). Pure read over the resolved registry.
pub fn report(&self) -> String {
let ext = self.declarations();
let registry = self.registry();
let mut out = String::new();
out.push_str("# JniGen binding report\n\n");
out.push_str(&format!(
"Base package: `{}`\n",
if ext.package.is_empty() {
"(none)"
} else {
&ext.package
}
));
// ── Packages: free functions + constants ─────────────────────────
for (subpackage, pkg_cfg) in &ext.packages {
let has_consts = !pkg_cfg.constants.is_empty()
|| !pkg_cfg.constant_functions.is_empty()
|| !pkg_cfg.constant_exprs.is_empty();
if pkg_cfg.functions.is_empty() && !has_consts {
continue;
}
let full = ext.package_name(subpackage);
out.push_str(&format!("\n## package `{full}`\n\n"));
let mut entries: Vec<&FunctionEntry> = pkg_cfg.functions.iter().collect();
entries.sort_by_key(|m| m.rust_ident.to_string());
for m in entries {
let name = ext.effective_function_name(subpackage, m);
self.report_fn(&mut out, &m.rust_ident, Some(&name), None);
}
let mut consts: Vec<String> = pkg_cfg
.constants
.iter()
.map(|c| {
format!(
"- `val {}` — `#[prebindgen]` const `{}`\n",
c.kotlin_name_override
.clone()
.unwrap_or_else(|| c.rust_ident.to_string()),
c.rust_ident
)
})
.chain(pkg_cfg.constant_functions.iter().map(|c| {
format!(
"- `val {}` — nullary `#[prebindgen]` fn `{}`\n",
c.kotlin_name_override
.clone()
.unwrap_or_else(|| c.rust_ident.to_string()),
c.rust_ident
)
}))
.chain(pkg_cfg.constant_exprs.iter().map(|e| {
format!(
"- `val {}: {}` — binding expression\n",
e.kotlin_name,
e.ty.to_token_stream()
)
}))
.collect();
consts.sort();
for c in consts {
out.push_str(&c);
}
}
// ── Classes: members ──────────────────────────────────────────────
let mut class_keys: Vec<&TypeKey> = ext.class_members.keys().collect();
class_keys.sort_by(|a, b| a.as_str().cmp(b.as_str()));
for key in class_keys {
let members = &ext.class_members[key];
if members.is_empty() {
continue;
}
let fqn = ext
.kotlin_fqn(key)
.unwrap_or_else(|| key.as_str().to_string());
out.push_str(&format!(
"\n## class `{fqn}` ({}, Rust `{}`)\n\n",
ext.class_kind_name(key),
key.as_str()
));
let mut ms: Vec<&ClassMember> = members.iter().collect();
ms.sort_by_key(|m| m.rust_ident.to_string());
for m in ms {
let name = ext.effective_method_name(key, m);
let receiver = match m.kind {
MemberKind::Method => Some(key),
MemberKind::Constructor => None,
};
self.report_fn(&mut out, &m.rust_ident, Some(&name), receiver);
}
}
// ── Types ────────────────────────────────────────────────────────
out.push_str("\n## types\n\n");
let mut type_keys: Vec<&TypeKey> = ext.types.keys().collect();
type_keys.sort_by(|a, b| a.as_str().cmp(b.as_str()));
for key in type_keys {
let cfg = &ext.types[key];
if cfg.name_spec.is_none() {
continue;
}
let fqn = ext
.kotlin_fqn(key)
.unwrap_or_else(|| key.as_str().to_string());
let wire = registry
.reading(key)
.and_then(|tr| registry.output_entry(&tr))
.map(|e| e.wire_type().to_token_stream().to_string())
.unwrap_or_else(|| "?".to_string());
out.push_str(&format!(
"- `{}`: {} → `{fqn}` (wire `{wire}`{})\n",
key.as_str(),
ext.class_kind_name(key),
if cfg.jobject_input {
", input `JObject` opt-in"
} else {
""
},
));
}
// Conversion sources (`convert!` decls) — the canonical single-value
// aspect, reported once per type rather than per function.
let mut convs: Vec<String> = ext
.convert_decls
.iter()
.map(|d| {
format!(
"- `convert!({})`{}\n",
d.key().as_str(),
d.describe_sources()
)
})
.collect();
convs.sort();
if !convs.is_empty() {
out.push_str("\n## conversions\n\n");
for c in convs {
out.push_str(&c);
}
}
// Rust-side-only boundary types (expand decls on undeclared types).
// A `sealed_class` sum is boundary-only too — it crosses flattened,
// not as one wire — but it very much materializes in Kotlin, so it is
// listed above with the other declared classes, not here.
let mut boundary: Vec<String> = ext
.rust_side_only_types()
.map(|(k, _)| format!("- `{}` (never materializes in Kotlin)\n", k.as_str()))
.collect();
boundary.sort();
if !boundary.is_empty() {
out.push_str("\n## rust-side-only types\n\n");
for b in boundary {
out.push_str(&b);
}
}
out
}
/// One function entry: the final Kotlin signature (same render path as
/// the emitters) + the plans that shaped it.
fn report_fn(
&self,
out: &mut String,
rust_ident: &syn::Ident,
kotlin_name: Option<&str>,
receiver_key: Option<&TypeKey>,
) {
let ext = self.declarations();
let registry = self.registry();
let Some(item_fn) = registry.flat().function(&rust_ident) else {
return;
};
let Some(f) = render_wrapper_fn(ext, item_fn, registry, kotlin_name, receiver_key) else {
return;
};
out.push_str(&format!("- `{}` — `{}`\n", rust_ident, signature(&f)));
// Param expansions.
let mut shaped: Vec<String> = Vec::new();
let mut plans: Vec<(&syn::Ident, &prebindgen_registry::expand::FoldPlan)> = registry
.expansion_plans()
.iter()
.filter(|((func, _), _)| func == rust_ident)
.map(|((_, param), plan)| (param, plan))
.collect();
plans.sort_by_key(|(p, _)| p.to_string());
for (param, plan) in plans {
let variants: Vec<String> = plan
.variants
.iter()
.map(|v| match &v.ctor {
Some(c) => c.to_string(),
None => "self".to_string(),
})
.collect();
shaped.push(format!(
"param `{param}` expanded from `{}` — variants [{}]",
plan.target,
variants.join(", ")
));
}
if let Some(plan) = registry.unfold_plans().get(rust_ident) {
let leaves: Vec<&str> = plan.leaves.iter().map(|l| l.name.as_str()).collect();
shaped.push(format!(
"return `{}` decomposed → [{}] ({:?} delivery)",
plan.source,
leaves.join(", "),
plan.delivery
));
}
if let Some(plan) = registry.error_plans().get(rust_ident) {
let leaves: Vec<&str> = plan.leaves.iter().map(|l| l.name.as_str()).collect();
shaped.push(format!(
"domain error `{}` decomposed → onError [{}] (binding failures → onBindingError)",
plan.source,
leaves.join(", ")
));
}
for s in shaped {
out.push_str(&format!(" - shaped by: {s}\n"));
}
}
}
impl Declarations {
/// Human-readable class-kind name of a declared type (report use).
pub(crate) fn class_kind_name(&self, key: &TypeKey) -> &'static str {
let Some(cfg) = self.types.get(key) else {
return "undeclared";
};
cfg.kind.macro_name()
}
}
/// `fun <generics> name(params): ret` off the public [`KtFun`] fields —
/// the signature only, no body/annotations (they are emission detail).
fn signature(f: &KtFun) -> String {
// The surface carries full-FQN types; render them through a throwaway
// `ImportSet` so the report shows short names (the imports are discarded).
let mut imports = kt::ImportSet::new("");
let generics = if f.generics.is_empty() {
String::new()
} else {
format!("<{}> ", f.generics.join(", "))
};
let params: Vec<String> = f
.params
.iter()
.map(|p| format!("{}: {}", p.name, p.ty.render(&mut imports)))
.collect();
let ret = match &f.ret {
Some(t) => format!(": {}", t.render(&mut imports)),
None => String::new(),
};
format!("fun {generics}{}({}){ret}", f.name, params.join(", "))
}