Skip to main content

alef_backend_java/
gen_visitor.rs

1/// Generate Java visitor support: interface, NodeContext record, VisitResult sealed interface,
2/// VisitorBridge (upcall stubs), and convertWithVisitor method.
3///
4/// # Panama FFM upcall strategy
5///
6/// Java cannot expose method references as raw C function pointers. The generated
7/// code uses Java 21+ Foreign Function & Memory API (Panama) upcall stubs:
8///
9/// - `NodeContext`: a `record` carrying the fields from `HTMHtmNodeContext`.
10/// - `VisitResult`: a `sealed interface` with `Continue`, `Skip`, `PreserveHtml`,
11///   `Custom`, and `Error` implementations.
12/// - `Visitor`: an `interface` with default no-op methods for all 40 callbacks.
13/// - `VisitorBridge`: a package-private class that allocates one `MemorySegment`
14///   upcall stub per callback inside a `Arena.ofConfined()` scope, then writes
15///   all stubs into a flat `MemorySegment` matching `HTMHtmVisitorCallbacks`.
16/// - `convertWithVisitor`: static method on the wrapper class that drives the full
17///   lifecycle: marshal options → create `VisitorBridge` → `htm_visitor_create` →
18///   `htm_convert_with_visitor` → deserialise JSON result → `htm_visitor_free`.
19use std::fmt::Write;
20
21// ---------------------------------------------------------------------------
22// Callback specification table (mirrors crates/alef-backend-go/src/gen_visitor.rs)
23// ---------------------------------------------------------------------------
24
25pub struct CallbackSpec {
26    /// Field name in `HTMHtmVisitorCallbacks` (snake_case). Used for documentation.
27    pub c_field: &'static str,
28    /// Java interface method name (camelCase).
29    pub java_method: &'static str,
30    /// Javadoc line.
31    pub doc: &'static str,
32    /// Extra parameters beyond `NodeContext` in the Java interface.
33    pub extra: &'static [ExtraParam],
34    /// If true, add `boolean isHeader` (only visit_table_row).
35    pub has_is_header: bool,
36}
37
38pub struct ExtraParam {
39    /// Java parameter name in the interface.
40    pub java_name: &'static str,
41    /// Java type in the interface method signature.
42    pub java_type: &'static str,
43    /// Panama `ValueLayout` constants for each C-level argument that maps to this Java param.
44    /// One Java param can correspond to multiple C args (e.g. cells = ptr + count).
45    pub c_layouts: &'static [&'static str],
46    /// Java expression to build the interface-typed value from the raw C parameters.
47    /// Raw variables are named `raw_<java_name>_<idx>` where idx counts within c_layouts.
48    pub decode: &'static str,
49}
50
51pub const CALLBACKS: &[CallbackSpec] = &[
52    CallbackSpec {
53        c_field: "visit_text",
54        java_method: "visitText",
55        doc: "Called for text nodes.",
56        extra: &[ExtraParam {
57            java_name: "text",
58            java_type: "String",
59            c_layouts: &["ValueLayout.ADDRESS"],
60            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
61        }],
62        has_is_header: false,
63    },
64    CallbackSpec {
65        c_field: "visit_element_start",
66        java_method: "visitElementStart",
67        doc: "Called before entering any element.",
68        extra: &[],
69        has_is_header: false,
70    },
71    CallbackSpec {
72        c_field: "visit_element_end",
73        java_method: "visitElementEnd",
74        doc: "Called after exiting any element; receives the default markdown output.",
75        extra: &[ExtraParam {
76            java_name: "output",
77            java_type: "String",
78            c_layouts: &["ValueLayout.ADDRESS"],
79            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
80        }],
81        has_is_header: false,
82    },
83    CallbackSpec {
84        c_field: "visit_link",
85        java_method: "visitLink",
86        doc: "Called for anchor links. title is null when the attribute is absent.",
87        extra: &[
88            ExtraParam {
89                java_name: "href",
90                java_type: "String",
91                c_layouts: &["ValueLayout.ADDRESS"],
92                decode: "raw_href_0.reinterpret(Long.MAX_VALUE).getString(0)",
93            },
94            ExtraParam {
95                java_name: "text",
96                java_type: "String",
97                c_layouts: &["ValueLayout.ADDRESS"],
98                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
99            },
100            ExtraParam {
101                java_name: "title",
102                java_type: "String",
103                c_layouts: &["ValueLayout.ADDRESS"],
104                decode: "raw_title_0.equals(MemorySegment.NULL) ? null : raw_title_0.reinterpret(Long.MAX_VALUE).getString(0)",
105            },
106        ],
107        has_is_header: false,
108    },
109    CallbackSpec {
110        c_field: "visit_image",
111        java_method: "visitImage",
112        doc: "Called for images. title is null when absent.",
113        extra: &[
114            ExtraParam {
115                java_name: "src",
116                java_type: "String",
117                c_layouts: &["ValueLayout.ADDRESS"],
118                decode: "raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
119            },
120            ExtraParam {
121                java_name: "alt",
122                java_type: "String",
123                c_layouts: &["ValueLayout.ADDRESS"],
124                decode: "raw_alt_0.reinterpret(Long.MAX_VALUE).getString(0)",
125            },
126            ExtraParam {
127                java_name: "title",
128                java_type: "String",
129                c_layouts: &["ValueLayout.ADDRESS"],
130                decode: "raw_title_0.equals(MemorySegment.NULL) ? null : raw_title_0.reinterpret(Long.MAX_VALUE).getString(0)",
131            },
132        ],
133        has_is_header: false,
134    },
135    CallbackSpec {
136        c_field: "visit_heading",
137        java_method: "visitHeading",
138        doc: "Called for heading elements h1-h6. id is null when absent.",
139        extra: &[
140            ExtraParam {
141                java_name: "level",
142                java_type: "int",
143                c_layouts: &["ValueLayout.JAVA_INT"],
144                decode: "(int) raw_level_0",
145            },
146            ExtraParam {
147                java_name: "text",
148                java_type: "String",
149                c_layouts: &["ValueLayout.ADDRESS"],
150                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
151            },
152            ExtraParam {
153                java_name: "id",
154                java_type: "String",
155                c_layouts: &["ValueLayout.ADDRESS"],
156                decode: "raw_id_0.equals(MemorySegment.NULL) ? null : raw_id_0.reinterpret(Long.MAX_VALUE).getString(0)",
157            },
158        ],
159        has_is_header: false,
160    },
161    CallbackSpec {
162        c_field: "visit_code_block",
163        java_method: "visitCodeBlock",
164        doc: "Called for code blocks. lang is null when absent.",
165        extra: &[
166            ExtraParam {
167                java_name: "lang",
168                java_type: "String",
169                c_layouts: &["ValueLayout.ADDRESS"],
170                decode: "raw_lang_0.equals(MemorySegment.NULL) ? null : raw_lang_0.reinterpret(Long.MAX_VALUE).getString(0)",
171            },
172            ExtraParam {
173                java_name: "code",
174                java_type: "String",
175                c_layouts: &["ValueLayout.ADDRESS"],
176                decode: "raw_code_0.reinterpret(Long.MAX_VALUE).getString(0)",
177            },
178        ],
179        has_is_header: false,
180    },
181    CallbackSpec {
182        c_field: "visit_code_inline",
183        java_method: "visitCodeInline",
184        doc: "Called for inline code elements.",
185        extra: &[ExtraParam {
186            java_name: "code",
187            java_type: "String",
188            c_layouts: &["ValueLayout.ADDRESS"],
189            decode: "raw_code_0.reinterpret(Long.MAX_VALUE).getString(0)",
190        }],
191        has_is_header: false,
192    },
193    CallbackSpec {
194        c_field: "visit_list_item",
195        java_method: "visitListItem",
196        doc: "Called for list items.",
197        extra: &[
198            ExtraParam {
199                java_name: "ordered",
200                java_type: "boolean",
201                c_layouts: &["ValueLayout.JAVA_INT"],
202                decode: "((int) raw_ordered_0) != 0",
203            },
204            ExtraParam {
205                java_name: "marker",
206                java_type: "String",
207                c_layouts: &["ValueLayout.ADDRESS"],
208                decode: "raw_marker_0.reinterpret(Long.MAX_VALUE).getString(0)",
209            },
210            ExtraParam {
211                java_name: "text",
212                java_type: "String",
213                c_layouts: &["ValueLayout.ADDRESS"],
214                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
215            },
216        ],
217        has_is_header: false,
218    },
219    CallbackSpec {
220        c_field: "visit_list_start",
221        java_method: "visitListStart",
222        doc: "Called before processing a list.",
223        extra: &[ExtraParam {
224            java_name: "ordered",
225            java_type: "boolean",
226            c_layouts: &["ValueLayout.JAVA_INT"],
227            decode: "((int) raw_ordered_0) != 0",
228        }],
229        has_is_header: false,
230    },
231    CallbackSpec {
232        c_field: "visit_list_end",
233        java_method: "visitListEnd",
234        doc: "Called after processing a list.",
235        extra: &[
236            ExtraParam {
237                java_name: "ordered",
238                java_type: "boolean",
239                c_layouts: &["ValueLayout.JAVA_INT"],
240                decode: "((int) raw_ordered_0) != 0",
241            },
242            ExtraParam {
243                java_name: "output",
244                java_type: "String",
245                c_layouts: &["ValueLayout.ADDRESS"],
246                decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
247            },
248        ],
249        has_is_header: false,
250    },
251    CallbackSpec {
252        c_field: "visit_table_start",
253        java_method: "visitTableStart",
254        doc: "Called before processing a table.",
255        extra: &[],
256        has_is_header: false,
257    },
258    CallbackSpec {
259        c_field: "visit_table_row",
260        java_method: "visitTableRow",
261        doc: "Called for table rows. cells contains the cell text values.",
262        extra: &[ExtraParam {
263            java_name: "cells",
264            java_type: "java.util.List<String>",
265            c_layouts: &["ValueLayout.ADDRESS", "ValueLayout.JAVA_LONG"],
266            decode: "decodeCells(raw_cells_0, (long) raw_cells_1)",
267        }],
268        has_is_header: true,
269    },
270    CallbackSpec {
271        c_field: "visit_table_end",
272        java_method: "visitTableEnd",
273        doc: "Called after processing a table.",
274        extra: &[ExtraParam {
275            java_name: "output",
276            java_type: "String",
277            c_layouts: &["ValueLayout.ADDRESS"],
278            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
279        }],
280        has_is_header: false,
281    },
282    CallbackSpec {
283        c_field: "visit_blockquote",
284        java_method: "visitBlockquote",
285        doc: "Called for blockquote elements.",
286        extra: &[
287            ExtraParam {
288                java_name: "content",
289                java_type: "String",
290                c_layouts: &["ValueLayout.ADDRESS"],
291                decode: "raw_content_0.reinterpret(Long.MAX_VALUE).getString(0)",
292            },
293            ExtraParam {
294                java_name: "depth",
295                java_type: "long",
296                c_layouts: &["ValueLayout.JAVA_LONG"],
297                decode: "(long) raw_depth_0",
298            },
299        ],
300        has_is_header: false,
301    },
302    CallbackSpec {
303        c_field: "visit_strong",
304        java_method: "visitStrong",
305        doc: "Called for strong/bold elements.",
306        extra: &[ExtraParam {
307            java_name: "text",
308            java_type: "String",
309            c_layouts: &["ValueLayout.ADDRESS"],
310            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
311        }],
312        has_is_header: false,
313    },
314    CallbackSpec {
315        c_field: "visit_emphasis",
316        java_method: "visitEmphasis",
317        doc: "Called for emphasis/italic elements.",
318        extra: &[ExtraParam {
319            java_name: "text",
320            java_type: "String",
321            c_layouts: &["ValueLayout.ADDRESS"],
322            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
323        }],
324        has_is_header: false,
325    },
326    CallbackSpec {
327        c_field: "visit_strikethrough",
328        java_method: "visitStrikethrough",
329        doc: "Called for strikethrough elements.",
330        extra: &[ExtraParam {
331            java_name: "text",
332            java_type: "String",
333            c_layouts: &["ValueLayout.ADDRESS"],
334            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
335        }],
336        has_is_header: false,
337    },
338    CallbackSpec {
339        c_field: "visit_underline",
340        java_method: "visitUnderline",
341        doc: "Called for underline elements.",
342        extra: &[ExtraParam {
343            java_name: "text",
344            java_type: "String",
345            c_layouts: &["ValueLayout.ADDRESS"],
346            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
347        }],
348        has_is_header: false,
349    },
350    CallbackSpec {
351        c_field: "visit_subscript",
352        java_method: "visitSubscript",
353        doc: "Called for subscript elements.",
354        extra: &[ExtraParam {
355            java_name: "text",
356            java_type: "String",
357            c_layouts: &["ValueLayout.ADDRESS"],
358            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
359        }],
360        has_is_header: false,
361    },
362    CallbackSpec {
363        c_field: "visit_superscript",
364        java_method: "visitSuperscript",
365        doc: "Called for superscript elements.",
366        extra: &[ExtraParam {
367            java_name: "text",
368            java_type: "String",
369            c_layouts: &["ValueLayout.ADDRESS"],
370            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
371        }],
372        has_is_header: false,
373    },
374    CallbackSpec {
375        c_field: "visit_mark",
376        java_method: "visitMark",
377        doc: "Called for mark/highlight elements.",
378        extra: &[ExtraParam {
379            java_name: "text",
380            java_type: "String",
381            c_layouts: &["ValueLayout.ADDRESS"],
382            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
383        }],
384        has_is_header: false,
385    },
386    CallbackSpec {
387        c_field: "visit_line_break",
388        java_method: "visitLineBreak",
389        doc: "Called for line break elements.",
390        extra: &[],
391        has_is_header: false,
392    },
393    CallbackSpec {
394        c_field: "visit_horizontal_rule",
395        java_method: "visitHorizontalRule",
396        doc: "Called for horizontal rule elements.",
397        extra: &[],
398        has_is_header: false,
399    },
400    CallbackSpec {
401        c_field: "visit_custom_element",
402        java_method: "visitCustomElement",
403        doc: "Called for custom or unknown elements.",
404        extra: &[
405            ExtraParam {
406                java_name: "tagName",
407                java_type: "String",
408                c_layouts: &["ValueLayout.ADDRESS"],
409                decode: "raw_tagName_0.reinterpret(Long.MAX_VALUE).getString(0)",
410            },
411            ExtraParam {
412                java_name: "html",
413                java_type: "String",
414                c_layouts: &["ValueLayout.ADDRESS"],
415                decode: "raw_html_0.reinterpret(Long.MAX_VALUE).getString(0)",
416            },
417        ],
418        has_is_header: false,
419    },
420    CallbackSpec {
421        c_field: "visit_definition_list_start",
422        java_method: "visitDefinitionListStart",
423        doc: "Called before a definition list.",
424        extra: &[],
425        has_is_header: false,
426    },
427    CallbackSpec {
428        c_field: "visit_definition_term",
429        java_method: "visitDefinitionTerm",
430        doc: "Called for definition term elements.",
431        extra: &[ExtraParam {
432            java_name: "text",
433            java_type: "String",
434            c_layouts: &["ValueLayout.ADDRESS"],
435            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
436        }],
437        has_is_header: false,
438    },
439    CallbackSpec {
440        c_field: "visit_definition_description",
441        java_method: "visitDefinitionDescription",
442        doc: "Called for definition description elements.",
443        extra: &[ExtraParam {
444            java_name: "text",
445            java_type: "String",
446            c_layouts: &["ValueLayout.ADDRESS"],
447            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
448        }],
449        has_is_header: false,
450    },
451    CallbackSpec {
452        c_field: "visit_definition_list_end",
453        java_method: "visitDefinitionListEnd",
454        doc: "Called after a definition list.",
455        extra: &[ExtraParam {
456            java_name: "output",
457            java_type: "String",
458            c_layouts: &["ValueLayout.ADDRESS"],
459            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
460        }],
461        has_is_header: false,
462    },
463    CallbackSpec {
464        c_field: "visit_form",
465        java_method: "visitForm",
466        doc: "Called for form elements. action and method may be null.",
467        extra: &[
468            ExtraParam {
469                java_name: "action",
470                java_type: "String",
471                c_layouts: &["ValueLayout.ADDRESS"],
472                decode: "raw_action_0.equals(MemorySegment.NULL) ? null : raw_action_0.reinterpret(Long.MAX_VALUE).getString(0)",
473            },
474            ExtraParam {
475                java_name: "method",
476                java_type: "String",
477                c_layouts: &["ValueLayout.ADDRESS"],
478                decode: "raw_method_0.equals(MemorySegment.NULL) ? null : raw_method_0.reinterpret(Long.MAX_VALUE).getString(0)",
479            },
480        ],
481        has_is_header: false,
482    },
483    CallbackSpec {
484        c_field: "visit_input",
485        java_method: "visitInput",
486        doc: "Called for input elements. name and value may be null.",
487        extra: &[
488            ExtraParam {
489                java_name: "inputType",
490                java_type: "String",
491                c_layouts: &["ValueLayout.ADDRESS"],
492                decode: "raw_inputType_0.reinterpret(Long.MAX_VALUE).getString(0)",
493            },
494            ExtraParam {
495                java_name: "name",
496                java_type: "String",
497                c_layouts: &["ValueLayout.ADDRESS"],
498                decode: "raw_name_0.equals(MemorySegment.NULL) ? null : raw_name_0.reinterpret(Long.MAX_VALUE).getString(0)",
499            },
500            ExtraParam {
501                java_name: "value",
502                java_type: "String",
503                c_layouts: &["ValueLayout.ADDRESS"],
504                decode: "raw_value_0.equals(MemorySegment.NULL) ? null : raw_value_0.reinterpret(Long.MAX_VALUE).getString(0)",
505            },
506        ],
507        has_is_header: false,
508    },
509    CallbackSpec {
510        c_field: "visit_button",
511        java_method: "visitButton",
512        doc: "Called for button elements.",
513        extra: &[ExtraParam {
514            java_name: "text",
515            java_type: "String",
516            c_layouts: &["ValueLayout.ADDRESS"],
517            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
518        }],
519        has_is_header: false,
520    },
521    CallbackSpec {
522        c_field: "visit_audio",
523        java_method: "visitAudio",
524        doc: "Called for audio elements. src may be null.",
525        extra: &[ExtraParam {
526            java_name: "src",
527            java_type: "String",
528            c_layouts: &["ValueLayout.ADDRESS"],
529            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
530        }],
531        has_is_header: false,
532    },
533    CallbackSpec {
534        c_field: "visit_video",
535        java_method: "visitVideo",
536        doc: "Called for video elements. src may be null.",
537        extra: &[ExtraParam {
538            java_name: "src",
539            java_type: "String",
540            c_layouts: &["ValueLayout.ADDRESS"],
541            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
542        }],
543        has_is_header: false,
544    },
545    CallbackSpec {
546        c_field: "visit_iframe",
547        java_method: "visitIframe",
548        doc: "Called for iframe elements. src may be null.",
549        extra: &[ExtraParam {
550            java_name: "src",
551            java_type: "String",
552            c_layouts: &["ValueLayout.ADDRESS"],
553            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
554        }],
555        has_is_header: false,
556    },
557    CallbackSpec {
558        c_field: "visit_details",
559        java_method: "visitDetails",
560        doc: "Called for details elements.",
561        extra: &[ExtraParam {
562            java_name: "open",
563            java_type: "boolean",
564            c_layouts: &["ValueLayout.JAVA_INT"],
565            decode: "((int) raw_open_0) != 0",
566        }],
567        has_is_header: false,
568    },
569    CallbackSpec {
570        c_field: "visit_summary",
571        java_method: "visitSummary",
572        doc: "Called for summary elements.",
573        extra: &[ExtraParam {
574            java_name: "text",
575            java_type: "String",
576            c_layouts: &["ValueLayout.ADDRESS"],
577            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
578        }],
579        has_is_header: false,
580    },
581    CallbackSpec {
582        c_field: "visit_figure_start",
583        java_method: "visitFigureStart",
584        doc: "Called before a figure element.",
585        extra: &[],
586        has_is_header: false,
587    },
588    CallbackSpec {
589        c_field: "visit_figcaption",
590        java_method: "visitFigcaption",
591        doc: "Called for figcaption elements.",
592        extra: &[ExtraParam {
593            java_name: "text",
594            java_type: "String",
595            c_layouts: &["ValueLayout.ADDRESS"],
596            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
597        }],
598        has_is_header: false,
599    },
600    CallbackSpec {
601        c_field: "visit_figure_end",
602        java_method: "visitFigureEnd",
603        doc: "Called after a figure element.",
604        extra: &[ExtraParam {
605            java_name: "output",
606            java_type: "String",
607            c_layouts: &["ValueLayout.ADDRESS"],
608            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
609        }],
610        has_is_header: false,
611    },
612];
613
614// ---------------------------------------------------------------------------
615// Public API: generate visitor-related Java source files
616// ---------------------------------------------------------------------------
617
618/// Returns `(filename, content)` pairs for all visitor-related Java files.
619///
620/// Callers push these into the `files` vector in `generate_bindings`.
621pub fn gen_visitor_files(package: &str, class_name: &str) -> Vec<(String, String)> {
622    vec![
623        ("NodeContext.java".to_string(), gen_node_context(package)),
624        ("VisitResult.java".to_string(), gen_visit_result(package)),
625        ("Visitor.java".to_string(), gen_visitor_interface(package, class_name)),
626        (
627            "VisitorBridge.java".to_string(),
628            gen_visitor_bridge(package, class_name),
629        ),
630    ]
631}
632
633/// Generate NativeLib method handle declarations for visitor FFI functions.
634///
635/// These lines are injected into the `NativeLib` class body after the normal handles.
636pub fn gen_native_lib_visitor_handles(prefix: &str) -> String {
637    let mut out = String::with_capacity(512);
638    let pu = prefix.to_uppercase();
639
640    writeln!(out).ok();
641    writeln!(out, "    // Visitor FFI handles").ok();
642    writeln!(
643        out,
644        "    static final MethodHandle {pu}_VISITOR_CREATE = LINKER.downcallHandle("
645    )
646    .ok();
647    writeln!(out, "        LIB.find(\"{prefix}_visitor_create\").orElseThrow(),").ok();
648    writeln!(
649        out,
650        "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
651    )
652    .ok();
653    writeln!(out, "    );").ok();
654    writeln!(out).ok();
655    writeln!(
656        out,
657        "    static final MethodHandle {pu}_VISITOR_FREE = LINKER.downcallHandle("
658    )
659    .ok();
660    writeln!(out, "        LIB.find(\"{prefix}_visitor_free\").orElseThrow(),").ok();
661    writeln!(out, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
662    writeln!(out, "    );").ok();
663    writeln!(out).ok();
664    writeln!(
665        out,
666        "    static final MethodHandle {pu}_CONVERT_WITH_VISITOR = LINKER.downcallHandle("
667    )
668    .ok();
669    writeln!(
670        out,
671        "        LIB.find(\"{prefix}_convert_with_visitor\").orElseThrow(),"
672    )
673    .ok();
674    writeln!(
675        out,
676        "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
677    )
678    .ok();
679    writeln!(out, "    );").ok();
680
681    out
682}
683
684/// Generate the `convertWithVisitor` method body to inject into the main wrapper class.
685///
686/// Returns the method source as a string (without surrounding class braces).
687pub fn gen_convert_with_visitor_method(class_name: &str, prefix: &str) -> String {
688    let mut out = String::with_capacity(2048);
689    let pu = prefix.to_uppercase();
690    let exc = format!("{class_name}Exception");
691
692    writeln!(
693        out,
694        "    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor) throws {exc} {{"
695    )
696    .ok();
697    writeln!(out, "        try (var arena = Arena.ofConfined();").ok();
698    writeln!(out, "             var bridge = new VisitorBridge(visitor)) {{").ok();
699    writeln!(out, "            var cHtml = arena.allocateFrom(html);").ok();
700    writeln!(out).ok();
701    writeln!(out, "            MemorySegment optionsPtr = MemorySegment.NULL;").ok();
702    writeln!(out, "            if (options != null) {{").ok();
703    writeln!(
704        out,
705        "                var optJson = arena.allocateFrom(createObjectMapper().writeValueAsString(options));"
706    )
707    .ok();
708    writeln!(
709        out,
710        "                optionsPtr = (MemorySegment) NativeLib.{pu}_CONVERSION_OPTIONS_FROM_JSON.invoke(optJson);"
711    )
712    .ok();
713    writeln!(out, "            }}").ok();
714    writeln!(out).ok();
715    writeln!(
716        out,
717        "            var visitorHandle = (MemorySegment) NativeLib.{pu}_VISITOR_CREATE.invoke(bridge.callbacksStruct());"
718    )
719    .ok();
720    writeln!(out, "            if (visitorHandle.equals(MemorySegment.NULL)) {{").ok();
721    writeln!(
722        out,
723        "                throw new {exc}(\"Failed to create visitor handle\", null);"
724    )
725    .ok();
726    writeln!(out, "            }}").ok();
727    writeln!(out).ok();
728    writeln!(out, "            try {{").ok();
729    writeln!(
730        out,
731        "                var resultPtr = (MemorySegment) NativeLib.{pu}_CONVERT_WITH_VISITOR.invoke(cHtml, optionsPtr, visitorHandle);"
732    )
733    .ok();
734    writeln!(out, "                if (!optionsPtr.equals(MemorySegment.NULL)) {{").ok();
735    writeln!(
736        out,
737        "                    NativeLib.{pu}_CONVERSION_OPTIONS_FREE.invoke(optionsPtr);"
738    )
739    .ok();
740    writeln!(out, "                }}").ok();
741    writeln!(out, "                if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
742    writeln!(out, "                    checkLastError();").ok();
743    writeln!(out, "                    return null;").ok();
744    writeln!(out, "                }}").ok();
745    writeln!(
746        out,
747        "                var json = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
748    )
749    .ok();
750    writeln!(out, "                NativeLib.{pu}_FREE_STRING.invoke(resultPtr);").ok();
751    writeln!(
752        out,
753        "                return createObjectMapper().readValue(json, ConversionResult.class);"
754    )
755    .ok();
756    writeln!(out, "            }} catch (Throwable e) {{").ok();
757    writeln!(out, "                throw new {exc}(\"FFI call failed\", e);").ok();
758    writeln!(out, "            }} finally {{").ok();
759    writeln!(
760        out,
761        "                NativeLib.{pu}_VISITOR_FREE.invoke(visitorHandle);"
762    )
763    .ok();
764    writeln!(out, "            }}").ok();
765    writeln!(out, "        }} catch ({exc} e) {{").ok();
766    writeln!(out, "            throw e;").ok();
767    writeln!(out, "        }} catch (Throwable e) {{").ok();
768    writeln!(out, "            throw new {exc}(\"FFI call failed\", e);").ok();
769    writeln!(out, "        }}").ok();
770    writeln!(out, "    }}").ok();
771
772    out
773}
774
775// ---------------------------------------------------------------------------
776// Individual file generators
777// ---------------------------------------------------------------------------
778
779fn gen_node_context(package: &str) -> String {
780    let mut out = String::with_capacity(1024);
781    writeln!(out, "// Code generated by alef. DO NOT EDIT.").ok();
782    writeln!(out, "package {package};").ok();
783    writeln!(out).ok();
784    writeln!(out, "/** Context passed to every visitor callback. */").ok();
785    writeln!(out, "public record NodeContext(").ok();
786    writeln!(out, "        /** Coarse-grained node type tag. */").ok();
787    writeln!(out, "        int nodeType,").ok();
788    writeln!(out, "        /** HTML element tag name (e.g. \"div\"). */").ok();
789    writeln!(out, "        String tagName,").ok();
790    writeln!(out, "        /** DOM depth (0 = root). */").ok();
791    writeln!(out, "        long depth,").ok();
792    writeln!(out, "        /** 0-based sibling index. */").ok();
793    writeln!(out, "        long indexInParent,").ok();
794    writeln!(out, "        /** Parent element tag name, or null at the root. */").ok();
795    writeln!(out, "        String parentTag,").ok();
796    writeln!(out, "        /** True when this element is treated as inline. */").ok();
797    writeln!(out, "        boolean isInline").ok();
798    writeln!(out, ") {{}}").ok();
799    out
800}
801
802fn gen_visit_result(package: &str) -> String {
803    let mut out = String::with_capacity(2048);
804    writeln!(out, "// Code generated by alef. DO NOT EDIT.").ok();
805    writeln!(out, "package {package};").ok();
806    writeln!(out).ok();
807    writeln!(out, "/** Controls how the visitor affects the conversion pipeline. */").ok();
808    writeln!(out, "public sealed interface VisitResult").ok();
809    writeln!(
810        out,
811        "        permits VisitResult.Continue, VisitResult.Skip, VisitResult.PreserveHtml,"
812    )
813    .ok();
814    writeln!(out, "                VisitResult.Custom, VisitResult.Error {{").ok();
815    writeln!(out).ok();
816    writeln!(out, "    /** Proceed with default conversion. */").ok();
817    writeln!(out, "    record Continue() implements VisitResult {{}}").ok();
818    writeln!(out).ok();
819    writeln!(out, "    /** Omit this element from output entirely. */").ok();
820    writeln!(out, "    record Skip() implements VisitResult {{}}").ok();
821    writeln!(out).ok();
822    writeln!(out, "    /** Keep original HTML verbatim. */").ok();
823    writeln!(out, "    record PreserveHtml() implements VisitResult {{}}").ok();
824    writeln!(out).ok();
825    writeln!(out, "    /** Replace with custom Markdown. */").ok();
826    writeln!(out, "    record Custom(String markdown) implements VisitResult {{}}").ok();
827    writeln!(out).ok();
828    writeln!(out, "    /** Abort conversion with an error message. */").ok();
829    writeln!(out, "    record Error(String message) implements VisitResult {{}}").ok();
830    writeln!(out).ok();
831    writeln!(out, "    /** Convenience: continue with default conversion. */").ok();
832    writeln!(
833        out,
834        "    static VisitResult continueDefault() {{ return new Continue(); }}"
835    )
836    .ok();
837    writeln!(out).ok();
838    writeln!(out, "    /** Convenience: skip this element. */").ok();
839    writeln!(out, "    static VisitResult skip() {{ return new Skip(); }}").ok();
840    writeln!(out).ok();
841    writeln!(out, "    /** Convenience: preserve original HTML. */").ok();
842    writeln!(
843        out,
844        "    static VisitResult preserveHtml() {{ return new PreserveHtml(); }}"
845    )
846    .ok();
847    writeln!(out).ok();
848    writeln!(out, "    /** Convenience: emit custom Markdown. */").ok();
849    writeln!(
850        out,
851        "    static VisitResult custom(String markdown) {{ return new Custom(markdown); }}"
852    )
853    .ok();
854    writeln!(out).ok();
855    writeln!(out, "    /** Convenience: abort with error. */").ok();
856    writeln!(
857        out,
858        "    static VisitResult error(String message) {{ return new Error(message); }}"
859    )
860    .ok();
861    writeln!(out, "}}").ok();
862    out
863}
864
865fn gen_visitor_interface(package: &str, _class_name: &str) -> String {
866    let mut out = String::with_capacity(4096);
867    writeln!(out, "// Code generated by alef. DO NOT EDIT.").ok();
868    writeln!(out, "package {package};").ok();
869    writeln!(out).ok();
870    writeln!(
871        out,
872        "/** Visitor interface for the HTML-to-Markdown conversion pipeline. */"
873    )
874    .ok();
875    writeln!(out, "public interface Visitor {{").ok();
876    for spec in CALLBACKS {
877        let params = iface_param_str(spec);
878        writeln!(out, "    /** {} */", spec.doc).ok();
879        writeln!(
880            out,
881            "    default VisitResult {}({}) {{ return VisitResult.continueDefault(); }}",
882            spec.java_method, params
883        )
884        .ok();
885    }
886    writeln!(out, "}}").ok();
887    out
888}
889
890/// Generate `VisitorBridge.java` — builds Panama upcall stubs for all 40 callbacks
891/// and exposes a `MemorySegment callbacksStruct()` pointing to the C struct.
892fn gen_visitor_bridge(package: &str, _class_name: &str) -> String {
893    let mut out = String::with_capacity(32_768);
894    writeln!(out, "// Code generated by alef. DO NOT EDIT.").ok();
895    writeln!(out, "package {package};").ok();
896    writeln!(out).ok();
897    writeln!(out, "import java.lang.foreign.Arena;").ok();
898    writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
899    writeln!(out, "import java.lang.foreign.Linker;").ok();
900    writeln!(out, "import java.lang.foreign.MemoryLayout;").ok();
901    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
902    writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
903    writeln!(out, "import java.lang.invoke.MethodHandles;").ok();
904    writeln!(out, "import java.lang.invoke.MethodType;").ok();
905    writeln!(out, "import java.util.ArrayList;").ok();
906    writeln!(out, "import java.util.List;").ok();
907    writeln!(out).ok();
908
909    writeln!(out, "/**").ok();
910    writeln!(out, " * Allocates Panama FFM upcall stubs for a Visitor and assembles").ok();
911    writeln!(out, " * the C HTMHtmVisitorCallbacks struct in native memory.").ok();
912    writeln!(out, " */").ok();
913    writeln!(out, "final class VisitorBridge implements AutoCloseable {{").ok();
914    writeln!(out, "    private static final Linker LINKER = Linker.nativeLinker();").ok();
915    writeln!(out, "    private static final MethodHandles.Lookup LOOKUP =").ok();
916    writeln!(out, "        MethodHandles.lookup();").ok();
917    writeln!(out).ok();
918    // Named constants for VisitResult discriminant values
919    writeln!(out, "    // VisitResult discriminant codes returned to C").ok();
920    writeln!(out, "    private static final int VISIT_RESULT_CONTINUE = 0;").ok();
921    writeln!(out, "    private static final int VISIT_RESULT_SKIP = 1;").ok();
922    writeln!(out, "    private static final int VISIT_RESULT_PRESERVE_HTML = 2;").ok();
923    writeln!(out, "    private static final int VISIT_RESULT_CUSTOM = 3;").ok();
924    writeln!(out, "    private static final int VISIT_RESULT_ERROR = 4;").ok();
925    writeln!(out).ok();
926
927    // The struct has user_data (pointer) + 40 function pointer fields.
928    let num_fields = CALLBACKS.len() + 1; // +1 for user_data
929    writeln!(
930        out,
931        "    // HTMHtmVisitorCallbacks: user_data + {n} callbacks",
932        n = CALLBACKS.len(),
933    )
934    .ok();
935    writeln!(out, "    // = {total} pointer-sized slots", total = num_fields,).ok();
936    writeln!(out, "    private static final long CALLBACKS_STRUCT_SIZE =").ok();
937    writeln!(out, "        (long) ValueLayout.ADDRESS.byteSize() * {num_fields}L;").ok();
938    writeln!(out).ok();
939    // Named offset constants for HTMHtmNodeContext struct fields (avoids magic numbers)
940    writeln!(out, "    // HTMHtmNodeContext field offsets").ok();
941    writeln!(out, "    private static final long CTX_OFFSET_TAG_NAME = 8L;").ok();
942    writeln!(out, "    private static final long CTX_OFFSET_DEPTH = 16L;").ok();
943    writeln!(out, "    private static final long CTX_OFFSET_INDEX_IN_PARENT = 24L;").ok();
944    writeln!(out, "    private static final long CTX_OFFSET_PARENT_TAG = 32L;").ok();
945    writeln!(out, "    private static final long CTX_OFFSET_IS_INLINE = 40L;").ok();
946    writeln!(out).ok();
947    writeln!(out, "    private final Arena arena;").ok();
948    writeln!(out, "    private final MemorySegment struct;").ok();
949    writeln!(out, "    private final Visitor visitor;").ok();
950    writeln!(out).ok();
951    writeln!(out, "    VisitorBridge(Visitor visitor) {{").ok();
952    writeln!(out, "        this.visitor = visitor;").ok();
953    writeln!(out, "        this.arena = Arena.ofConfined();").ok();
954    writeln!(out, "        this.struct = arena.allocate(CALLBACKS_STRUCT_SIZE);").ok();
955    writeln!(out, "        // Slot 0: user_data = NULL").ok();
956    writeln!(out, "        // (visitor captured via lambda closure)").ok();
957    writeln!(out, "        struct.set(ValueLayout.ADDRESS, 0L, MemorySegment.NULL);").ok();
958    writeln!(out, "        try {{").ok();
959    writeln!(out, "            long offset = ValueLayout.ADDRESS.byteSize();").ok();
960    // Compute number of registerStubs sub-methods needed
961    let num_chunks = CALLBACKS.chunks(10).count();
962    for i in 1..=num_chunks {
963        if i < num_chunks {
964            writeln!(out, "            offset = registerStubs{}(offset);", i).ok();
965        } else {
966            writeln!(out, "            registerStubs{}(offset);", i).ok();
967        }
968    }
969    writeln!(out, "        }} catch (ReflectiveOperationException e) {{").ok();
970    writeln!(out, "            arena.close();").ok();
971    writeln!(out, "            throw new RuntimeException(").ok();
972    writeln!(out, "                \"Failed to create visitor upcall stubs\", e);").ok();
973    writeln!(out, "        }}").ok();
974    writeln!(out, "    }}").ok();
975    writeln!(out).ok();
976
977    // Split callbacks into chunks of 10 each; each sub-method returns the updated offset.
978    const CHUNK_SIZE: usize = 10;
979    for (chunk_idx, chunk) in CALLBACKS.chunks(CHUNK_SIZE).enumerate() {
980        let method_num = chunk_idx + 1;
981        writeln!(
982            out,
983            "    private long registerStubs{}(\n            final long offset)\n            throws ReflectiveOperationException {{",
984            method_num
985        )
986        .ok();
987        writeln!(out, "        long off = offset;").ok();
988        for spec in chunk {
989            let descriptor = callback_descriptor(spec);
990            let method_type = callback_method_type(spec);
991            let stub_var = stub_var_name(spec.java_method);
992            writeln!(out, "        // {}", spec.c_field).ok();
993            writeln!(out, "        var {} = LINKER.upcallStub(", stub_var).ok();
994            writeln!(out, "                LOOKUP.bind(",).ok();
995            writeln!(
996                out,
997                "                    this, \"{}\",",
998                handle_method_name(spec.java_method),
999            )
1000            .ok();
1001            writeln!(out, "                    {}),", method_type).ok();
1002            writeln!(out, "                {},", descriptor).ok();
1003            writeln!(out, "                arena);").ok();
1004            writeln!(out, "        struct.set(ValueLayout.ADDRESS, off, {});", stub_var).ok();
1005            writeln!(out, "        off += ValueLayout.ADDRESS.byteSize();").ok();
1006        }
1007        writeln!(out, "        return off;").ok();
1008        writeln!(out, "    }}").ok();
1009        writeln!(out).ok();
1010    }
1011    writeln!(out).ok();
1012    writeln!(
1013        out,
1014        "    /** Returns the native HTMHtmVisitorCallbacks struct pointer. */"
1015    )
1016    .ok();
1017    writeln!(out, "    MemorySegment callbacksStruct() {{").ok();
1018    writeln!(out, "        return struct;").ok();
1019    writeln!(out, "    }}").ok();
1020    writeln!(out).ok();
1021
1022    // Generate one handle_* method per callback
1023    for spec in CALLBACKS {
1024        gen_handle_method(&mut out, spec);
1025    }
1026
1027    // decodeNodeContext helper
1028    writeln!(
1029        out,
1030        "    // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,"
1031    )
1032    .ok();
1033    writeln!(out, "    //   uintptr index_in_parent, char* parent_tag,").ok();
1034    writeln!(out, "    //   int32 is_inline").ok();
1035    writeln!(out, "    private static final MemoryLayout CTX_LAYOUT =").ok();
1036    writeln!(out, "        MemoryLayout.structLayout(").ok();
1037    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"node_type\"),").ok();
1038    writeln!(out, "            MemoryLayout.paddingLayout(4),").ok();
1039    writeln!(out, "            ValueLayout.ADDRESS.withName(\"tag_name\"),").ok();
1040    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"depth\"),").ok();
1041    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"index_in_parent\"),").ok();
1042    writeln!(out, "            ValueLayout.ADDRESS.withName(\"parent_tag\"),").ok();
1043    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"is_inline\"),").ok();
1044    writeln!(out, "            MemoryLayout.paddingLayout(4)").ok();
1045    writeln!(out, "    );").ok();
1046    writeln!(out).ok();
1047    writeln!(out, "    private static NodeContext decodeNodeContext(").ok();
1048    writeln!(out, "            final MemorySegment ctxPtr) {{").ok();
1049    writeln!(out, "        var ctx = ctxPtr.reinterpret(").ok();
1050    writeln!(out, "            CTX_LAYOUT.byteSize());").ok();
1051    writeln!(out, "        int nodeType = ctx.get(").ok();
1052    writeln!(out, "            ValueLayout.JAVA_INT, 0L);").ok();
1053    writeln!(out, "        var tagNamePtr = ctx.get(").ok();
1054    writeln!(out, "            ValueLayout.ADDRESS, CTX_OFFSET_TAG_NAME);").ok();
1055    writeln!(out, "        String tagName = tagNamePtr").ok();
1056    writeln!(out, "            .reinterpret(Long.MAX_VALUE).getString(0);").ok();
1057    writeln!(
1058        out,
1059        "        long depth = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_DEPTH);"
1060    )
1061    .ok();
1062    writeln!(
1063        out,
1064        "        long indexInParent = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_INDEX_IN_PARENT);"
1065    )
1066    .ok();
1067    writeln!(
1068        out,
1069        "        var parentTagPtr = ctx.get(ValueLayout.ADDRESS, CTX_OFFSET_PARENT_TAG);"
1070    )
1071    .ok();
1072    writeln!(
1073        out,
1074        "        String parentTag = parentTagPtr.equals(MemorySegment.NULL) ? null"
1075    )
1076    .ok();
1077    writeln!(
1078        out,
1079        "                : parentTagPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1080    )
1081    .ok();
1082    writeln!(
1083        out,
1084        "        int isInlineRaw = ctx.get(ValueLayout.JAVA_INT, CTX_OFFSET_IS_INLINE);"
1085    )
1086    .ok();
1087    writeln!(
1088        out,
1089        "        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);"
1090    )
1091    .ok();
1092    writeln!(out, "    }}").ok();
1093    writeln!(out).ok();
1094
1095    // decodeCells helper
1096    writeln!(
1097        out,
1098        "    private static List<String> decodeCells(MemorySegment cellsPtr, long count) {{"
1099    )
1100    .ok();
1101    writeln!(out, "        var result = new ArrayList<String>((int) count);").ok();
1102    writeln!(out, "        for (long i = 0; i < count; i++) {{").ok();
1103    writeln!(
1104        out,
1105        "            var ptr = cellsPtr.get(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize());"
1106    )
1107    .ok();
1108    writeln!(
1109        out,
1110        "            result.add(ptr.reinterpret(Long.MAX_VALUE).getString(0));"
1111    )
1112    .ok();
1113    writeln!(out, "        }}").ok();
1114    writeln!(out, "        return result;").ok();
1115    writeln!(out, "    }}").ok();
1116    writeln!(out).ok();
1117
1118    // encodeVisitResult helper
1119    writeln!(
1120        out,
1121        "    private static int encodeVisitResult(VisitResult result, MemorySegment outCustom, MemorySegment outLen, Arena encArena) {{"
1122    )
1123    .ok();
1124    writeln!(out, "        return switch (result) {{").ok();
1125    writeln!(
1126        out,
1127        "            case VisitResult.Continue ignored -> VISIT_RESULT_CONTINUE;"
1128    )
1129    .ok();
1130    writeln!(out, "            case VisitResult.Skip ignored -> VISIT_RESULT_SKIP;").ok();
1131    writeln!(
1132        out,
1133        "            case VisitResult.PreserveHtml ignored -> VISIT_RESULT_PRESERVE_HTML;"
1134    )
1135    .ok();
1136    writeln!(out, "            case VisitResult.Custom c -> {{").ok();
1137    writeln!(out, "                var buf = encArena.allocateFrom(c.markdown());").ok();
1138    writeln!(out, "                outCustom.set(ValueLayout.ADDRESS, 0L, buf);").ok();
1139    writeln!(
1140        out,
1141        "                outLen.set(ValueLayout.JAVA_LONG, 0L, (long) c.markdown().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1142    )
1143    .ok();
1144    writeln!(out, "                yield VISIT_RESULT_CUSTOM;").ok();
1145    writeln!(out, "            }}").ok();
1146    writeln!(out, "            case VisitResult.Error e -> {{").ok();
1147    writeln!(out, "                var buf = encArena.allocateFrom(e.message());").ok();
1148    writeln!(out, "                outCustom.set(ValueLayout.ADDRESS, 0L, buf);").ok();
1149    writeln!(
1150        out,
1151        "                outLen.set(ValueLayout.JAVA_LONG, 0L, (long) e.message().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1152    )
1153    .ok();
1154    writeln!(out, "                yield VISIT_RESULT_ERROR;").ok();
1155    writeln!(out, "            }}").ok();
1156    writeln!(out, "        }};").ok();
1157    writeln!(out, "    }}").ok();
1158    writeln!(out).ok();
1159
1160    writeln!(out, "    @Override").ok();
1161    writeln!(out, "    public void close() {{").ok();
1162    writeln!(out, "        arena.close();").ok();
1163    writeln!(out, "    }}").ok();
1164    writeln!(out, "}}").ok();
1165    out
1166}
1167
1168// ---------------------------------------------------------------------------
1169// Internal helpers
1170// ---------------------------------------------------------------------------
1171
1172/// Generate camelCase stub variable name: stub + capitalize(java_method).
1173/// e.g. visitText -> stubVisitText
1174fn stub_var_name(java_method: &str) -> String {
1175    let mut name = String::with_capacity(5 + java_method.len());
1176    name.push_str("stub");
1177    let mut chars = java_method.chars();
1178    if let Some(first) = chars.next() {
1179        for c in first.to_uppercase() {
1180            name.push(c);
1181        }
1182        name.push_str(chars.as_str());
1183    }
1184    name
1185}
1186
1187fn handle_method_name(java_method: &str) -> String {
1188    // camelCase: "handle" + capitalize first letter of java_method
1189    let mut name = String::with_capacity(7 + java_method.len());
1190    name.push_str("handle");
1191    let mut chars = java_method.chars();
1192    if let Some(first) = chars.next() {
1193        for c in first.to_uppercase() {
1194            name.push(c);
1195        }
1196        name.push_str(chars.as_str());
1197    }
1198    name
1199}
1200
1201fn iface_param_str(spec: &CallbackSpec) -> String {
1202    let mut params = vec!["final NodeContext context".to_string()];
1203    for ep in spec.extra {
1204        params.push(format!("final {} {}", ep.java_type, ep.java_name));
1205    }
1206    if spec.has_is_header {
1207        params.push("final boolean isHeader".to_string());
1208    }
1209    params.join(", ")
1210}
1211
1212/// Build the `FunctionDescriptor` for one callback's upcall stub.
1213/// All callbacks: (ADDRESS ctx, ADDRESS userData, ..extra.., ADDRESS outCustom, ADDRESS outLen) -> JAVA_INT
1214/// Returns a multi-line string with 20-space continuation indent so no line exceeds 80 chars.
1215fn callback_descriptor(spec: &CallbackSpec) -> String {
1216    let mut layouts = vec![
1217        "ValueLayout.ADDRESS".to_string(), // ctx
1218        "ValueLayout.ADDRESS".to_string(), // user_data
1219    ];
1220    for ep in spec.extra {
1221        for layout in ep.c_layouts {
1222            layouts.push((*layout).to_string());
1223        }
1224    }
1225    if spec.has_is_header {
1226        layouts.push("ValueLayout.JAVA_INT".to_string());
1227    }
1228    layouts.push("ValueLayout.ADDRESS".to_string()); // out_custom
1229    layouts.push("ValueLayout.ADDRESS".to_string()); // out_len
1230    let indent = "                    ";
1231    let args = layouts.join(&format!(",\n{indent}"));
1232    format!("FunctionDescriptor.of(\n{indent}ValueLayout.JAVA_INT,\n{indent}{args})")
1233}
1234
1235/// Build the `MethodType` for `LOOKUP.bind(this, name, type)`.
1236/// Returns a multi-line string with 20-space continuation indent so no line exceeds 80 chars.
1237fn callback_method_type(spec: &CallbackSpec) -> String {
1238    let mut types = vec![
1239        "MemorySegment.class".to_string(), // ctx
1240        "MemorySegment.class".to_string(), // user_data
1241    ];
1242    for ep in spec.extra {
1243        for layout in ep.c_layouts {
1244            types.push(layout_to_java_class(layout).to_string());
1245        }
1246    }
1247    if spec.has_is_header {
1248        types.push("int.class".to_string());
1249    }
1250    types.push("MemorySegment.class".to_string()); // out_custom
1251    types.push("MemorySegment.class".to_string()); // out_len
1252    let indent = "                    ";
1253    let args = types.join(&format!(",\n{indent}"));
1254    format!("MethodType.methodType(\n{indent}int.class,\n{indent}{args})")
1255}
1256
1257fn layout_to_java_class(layout: &str) -> &'static str {
1258    match layout {
1259        "ValueLayout.ADDRESS" => "MemorySegment.class",
1260        "ValueLayout.JAVA_INT" => "int.class",
1261        "ValueLayout.JAVA_LONG" => "long.class",
1262        _ => "long.class",
1263    }
1264}
1265
1266/// Generate one `handle_*` instance method inside `VisitorBridge`.
1267fn gen_handle_method(out: &mut String, spec: &CallbackSpec) {
1268    // Build method signature matching the MethodType passed to upcallStub.
1269    let mut params = vec![
1270        "final MemorySegment ctx".to_string(),
1271        "final MemorySegment userData".to_string(),
1272    ];
1273    for ep in spec.extra {
1274        for (c_idx, layout) in ep.c_layouts.iter().enumerate() {
1275            let java_ptype = match *layout {
1276                "ValueLayout.JAVA_INT" => "int",
1277                "ValueLayout.JAVA_LONG" => "long",
1278                _ => "MemorySegment",
1279            };
1280            params.push(format!("final {java_ptype} {}", raw_var_name(ep.java_name, c_idx)));
1281        }
1282    }
1283    if spec.has_is_header {
1284        params.push("final int isHeader".to_string());
1285    }
1286    params.push("final MemorySegment outCustom".to_string());
1287    params.push("final MemorySegment outLen".to_string());
1288
1289    let method_name = handle_method_name(spec.java_method);
1290    let single_line = format!("    int {}({}) {{", method_name, params.join(", "));
1291    if single_line.len() <= 80 {
1292        writeln!(out, "{}", single_line).ok();
1293    } else {
1294        let indent = "            ";
1295        writeln!(
1296            out,
1297            "    int {}(\n{}{}) {{",
1298            method_name,
1299            indent,
1300            params.join(&format!(",\n{indent}"))
1301        )
1302        .ok();
1303    }
1304    writeln!(out, "        try (var encArena = Arena.ofConfined()) {{").ok();
1305    writeln!(out, "            var context = decodeNodeContext(ctx);").ok();
1306
1307    // Decode each extra param
1308    for ep in spec.extra {
1309        let mut decode = ep.decode.to_string();
1310        for (c_idx, _) in ep.c_layouts.iter().enumerate() {
1311            let placeholder = format!("raw_{}_{}", ep.java_name, c_idx);
1312            let var = raw_var_name(ep.java_name, c_idx);
1313            decode = decode.replace(&placeholder, &var);
1314        }
1315        writeln!(out, "            var {} = {};", ep.java_name, decode).ok();
1316    }
1317    if spec.has_is_header {
1318        writeln!(out, "            var goIsHeader = isHeader != 0;").ok();
1319    }
1320
1321    // Build call args
1322    let mut call_args = vec!["context".to_string()];
1323    for ep in spec.extra {
1324        call_args.push(ep.java_name.to_string());
1325    }
1326    if spec.has_is_header {
1327        call_args.push("goIsHeader".to_string());
1328    }
1329
1330    writeln!(
1331        out,
1332        "            var result = visitor.{}({});",
1333        spec.java_method,
1334        call_args.join(", ")
1335    )
1336    .ok();
1337    writeln!(
1338        out,
1339        "            return encodeVisitResult(result, outCustom, outLen, encArena);"
1340    )
1341    .ok();
1342    writeln!(out, "        }} catch (Throwable ignored) {{").ok();
1343    writeln!(out, "            return 0;").ok();
1344    writeln!(out, "        }}").ok();
1345    writeln!(out, "    }}").ok();
1346    writeln!(out).ok();
1347}
1348
1349fn raw_var_name(java_name: &str, c_idx: usize) -> String {
1350    // camelCase: "raw" + capitalize first letter of java_name + "_" + index
1351    // e.g. raw_text_0 -> rawText0, raw_cells_1 -> rawCells1
1352    let mut name = String::with_capacity(4 + java_name.len() + 2);
1353    name.push_str("raw");
1354    let mut chars = java_name.chars();
1355    if let Some(first) = chars.next() {
1356        for c in first.to_uppercase() {
1357            name.push(c);
1358        }
1359        name.push_str(chars.as_str());
1360    }
1361    name.push_str(&c_idx.to_string());
1362    name
1363}