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!(
916        out,
917        "    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();"
918    )
919    .ok();
920    writeln!(out).ok();
921    // Named constants for VisitResult discriminant values
922    writeln!(out, "    // VisitResult discriminant codes returned to C").ok();
923    writeln!(out, "    private static final int VISIT_RESULT_CONTINUE = 0;").ok();
924    writeln!(out, "    private static final int VISIT_RESULT_SKIP = 1;").ok();
925    writeln!(out, "    private static final int VISIT_RESULT_PRESERVE_HTML = 2;").ok();
926    writeln!(out, "    private static final int VISIT_RESULT_CUSTOM = 3;").ok();
927    writeln!(out, "    private static final int VISIT_RESULT_ERROR = 4;").ok();
928    writeln!(out).ok();
929
930    // The struct has user_data (pointer) + 40 function pointer fields.
931    let num_fields = CALLBACKS.len() + 1; // +1 for user_data
932    writeln!(
933        out,
934        "    // HTMHtmVisitorCallbacks: user_data + {n} callback pointers = {total} pointer-sized slots",
935        n = CALLBACKS.len(),
936        total = num_fields,
937    )
938    .ok();
939    writeln!(
940        out,
941        "    private static final long CALLBACKS_STRUCT_SIZE = (long) ValueLayout.ADDRESS.byteSize() * {num_fields}L;"
942    )
943    .ok();
944    writeln!(out).ok();
945    // Named offset constants for HTMHtmNodeContext struct fields (avoids magic numbers)
946    writeln!(out, "    // HTMHtmNodeContext field offsets").ok();
947    writeln!(out, "    private static final long CTX_OFFSET_TAG_NAME = 8L;").ok();
948    writeln!(out, "    private static final long CTX_OFFSET_DEPTH = 16L;").ok();
949    writeln!(out, "    private static final long CTX_OFFSET_INDEX_IN_PARENT = 24L;").ok();
950    writeln!(out, "    private static final long CTX_OFFSET_PARENT_TAG = 32L;").ok();
951    writeln!(out, "    private static final long CTX_OFFSET_IS_INLINE = 40L;").ok();
952    writeln!(out).ok();
953    writeln!(out, "    private final Arena arena;").ok();
954    writeln!(out, "    private final MemorySegment struct;").ok();
955    writeln!(out, "    private final Visitor visitor;").ok();
956    writeln!(out).ok();
957    writeln!(out, "    VisitorBridge(Visitor visitor) {{").ok();
958    writeln!(out, "        this.visitor = visitor;").ok();
959    writeln!(out, "        this.arena = Arena.ofConfined();").ok();
960    writeln!(out, "        this.struct = arena.allocate(CALLBACKS_STRUCT_SIZE);").ok();
961    writeln!(
962        out,
963        "        // Slot 0: user_data = NULL (not needed; visitor captured via lambda closure)"
964    )
965    .ok();
966    writeln!(out, "        struct.set(ValueLayout.ADDRESS, 0L, MemorySegment.NULL);").ok();
967    writeln!(out, "        try {{").ok();
968    writeln!(out, "            long offset = ValueLayout.ADDRESS.byteSize();").ok();
969    // Compute number of registerStubs sub-methods needed
970    let num_chunks = CALLBACKS.chunks(10).count();
971    for i in 1..=num_chunks {
972        if i < num_chunks {
973            writeln!(out, "            offset = registerStubs{}(offset);", i).ok();
974        } else {
975            writeln!(out, "            registerStubs{}(offset);", i).ok();
976        }
977    }
978    writeln!(out, "        }} catch (ReflectiveOperationException e) {{").ok();
979    writeln!(out, "            arena.close();").ok();
980    writeln!(
981        out,
982        "            throw new RuntimeException(\"Failed to create visitor upcall stubs\", e);"
983    )
984    .ok();
985    writeln!(out, "        }}").ok();
986    writeln!(out, "    }}").ok();
987    writeln!(out).ok();
988
989    // Split callbacks into chunks of 10 each; each sub-method returns the updated offset.
990    const CHUNK_SIZE: usize = 10;
991    for (chunk_idx, chunk) in CALLBACKS.chunks(CHUNK_SIZE).enumerate() {
992        let method_num = chunk_idx + 1;
993        writeln!(
994            out,
995            "    private long registerStubs{}(long offset) throws ReflectiveOperationException {{",
996            method_num
997        )
998        .ok();
999        for spec in chunk {
1000            let descriptor = callback_descriptor(spec);
1001            let method_type = callback_method_type(spec);
1002            let stub_var = stub_var_name(spec.java_method);
1003            writeln!(out, "        // {}", spec.c_field).ok();
1004            writeln!(out, "        var {} = LINKER.upcallStub(", stub_var).ok();
1005            writeln!(
1006                out,
1007                "                LOOKUP.bind(this, \"{}\", {}),",
1008                handle_method_name(spec.java_method),
1009                method_type
1010            )
1011            .ok();
1012            writeln!(out, "                {},", descriptor).ok();
1013            writeln!(out, "                arena);").ok();
1014            writeln!(out, "        struct.set(ValueLayout.ADDRESS, offset, {});", stub_var).ok();
1015            writeln!(out, "        offset += ValueLayout.ADDRESS.byteSize();").ok();
1016        }
1017        writeln!(out, "        return offset;").ok();
1018        writeln!(out, "    }}").ok();
1019        writeln!(out).ok();
1020    }
1021    writeln!(out).ok();
1022    writeln!(
1023        out,
1024        "    /** Returns the native HTMHtmVisitorCallbacks struct pointer. */"
1025    )
1026    .ok();
1027    writeln!(out, "    MemorySegment callbacksStruct() {{").ok();
1028    writeln!(out, "        return struct;").ok();
1029    writeln!(out, "    }}").ok();
1030    writeln!(out).ok();
1031
1032    // Generate one handle_* method per callback
1033    for spec in CALLBACKS {
1034        gen_handle_method(&mut out, spec);
1035    }
1036
1037    // decodeNodeContext helper
1038    writeln!(
1039        out,
1040        "    // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,"
1041    )
1042    .ok();
1043    writeln!(
1044        out,
1045        "    //                    uintptr index_in_parent, char* parent_tag, int32 is_inline"
1046    )
1047    .ok();
1048    writeln!(
1049        out,
1050        "    private static final MemoryLayout CTX_LAYOUT = MemoryLayout.structLayout("
1051    )
1052    .ok();
1053    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"node_type\"),").ok();
1054    writeln!(out, "            MemoryLayout.paddingLayout(4),").ok();
1055    writeln!(out, "            ValueLayout.ADDRESS.withName(\"tag_name\"),").ok();
1056    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"depth\"),").ok();
1057    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"index_in_parent\"),").ok();
1058    writeln!(out, "            ValueLayout.ADDRESS.withName(\"parent_tag\"),").ok();
1059    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"is_inline\"),").ok();
1060    writeln!(out, "            MemoryLayout.paddingLayout(4)").ok();
1061    writeln!(out, "    );").ok();
1062    writeln!(out).ok();
1063    writeln!(
1064        out,
1065        "    private static NodeContext decodeNodeContext(MemorySegment ctxPtr) {{"
1066    )
1067    .ok();
1068    writeln!(out, "        var ctx = ctxPtr.reinterpret(CTX_LAYOUT.byteSize());").ok();
1069    writeln!(out, "        int nodeType = ctx.get(ValueLayout.JAVA_INT, 0L);").ok();
1070    writeln!(
1071        out,
1072        "        var tagNamePtr = ctx.get(ValueLayout.ADDRESS, CTX_OFFSET_TAG_NAME);"
1073    )
1074    .ok();
1075    writeln!(
1076        out,
1077        "        String tagName = tagNamePtr.reinterpret(Long.MAX_VALUE).getString(0);"
1078    )
1079    .ok();
1080    writeln!(
1081        out,
1082        "        long depth = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_DEPTH);"
1083    )
1084    .ok();
1085    writeln!(
1086        out,
1087        "        long indexInParent = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_INDEX_IN_PARENT);"
1088    )
1089    .ok();
1090    writeln!(
1091        out,
1092        "        var parentTagPtr = ctx.get(ValueLayout.ADDRESS, CTX_OFFSET_PARENT_TAG);"
1093    )
1094    .ok();
1095    writeln!(
1096        out,
1097        "        String parentTag = parentTagPtr.equals(MemorySegment.NULL) ? null"
1098    )
1099    .ok();
1100    writeln!(
1101        out,
1102        "                : parentTagPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1103    )
1104    .ok();
1105    writeln!(
1106        out,
1107        "        int isInlineRaw = ctx.get(ValueLayout.JAVA_INT, CTX_OFFSET_IS_INLINE);"
1108    )
1109    .ok();
1110    writeln!(
1111        out,
1112        "        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);"
1113    )
1114    .ok();
1115    writeln!(out, "    }}").ok();
1116    writeln!(out).ok();
1117
1118    // decodeCells helper
1119    writeln!(
1120        out,
1121        "    private static List<String> decodeCells(MemorySegment cellsPtr, long count) {{"
1122    )
1123    .ok();
1124    writeln!(out, "        var result = new ArrayList<String>((int) count);").ok();
1125    writeln!(out, "        for (long i = 0; i < count; i++) {{").ok();
1126    writeln!(
1127        out,
1128        "            var ptr = cellsPtr.get(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize());"
1129    )
1130    .ok();
1131    writeln!(
1132        out,
1133        "            result.add(ptr.reinterpret(Long.MAX_VALUE).getString(0));"
1134    )
1135    .ok();
1136    writeln!(out, "        }}").ok();
1137    writeln!(out, "        return result;").ok();
1138    writeln!(out, "    }}").ok();
1139    writeln!(out).ok();
1140
1141    // encodeVisitResult helper
1142    writeln!(
1143        out,
1144        "    private static int encodeVisitResult(VisitResult result, MemorySegment outCustom, MemorySegment outLen, Arena encArena) {{"
1145    )
1146    .ok();
1147    writeln!(out, "        return switch (result) {{").ok();
1148    writeln!(
1149        out,
1150        "            case VisitResult.Continue ignored -> VISIT_RESULT_CONTINUE;"
1151    )
1152    .ok();
1153    writeln!(out, "            case VisitResult.Skip ignored -> VISIT_RESULT_SKIP;").ok();
1154    writeln!(
1155        out,
1156        "            case VisitResult.PreserveHtml ignored -> VISIT_RESULT_PRESERVE_HTML;"
1157    )
1158    .ok();
1159    writeln!(out, "            case VisitResult.Custom c -> {{").ok();
1160    writeln!(out, "                var buf = encArena.allocateFrom(c.markdown());").ok();
1161    writeln!(out, "                outCustom.set(ValueLayout.ADDRESS, 0L, buf);").ok();
1162    writeln!(
1163        out,
1164        "                outLen.set(ValueLayout.JAVA_LONG, 0L, (long) c.markdown().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1165    )
1166    .ok();
1167    writeln!(out, "                yield VISIT_RESULT_CUSTOM;").ok();
1168    writeln!(out, "            }}").ok();
1169    writeln!(out, "            case VisitResult.Error e -> {{").ok();
1170    writeln!(out, "                var buf = encArena.allocateFrom(e.message());").ok();
1171    writeln!(out, "                outCustom.set(ValueLayout.ADDRESS, 0L, buf);").ok();
1172    writeln!(
1173        out,
1174        "                outLen.set(ValueLayout.JAVA_LONG, 0L, (long) e.message().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1175    )
1176    .ok();
1177    writeln!(out, "                yield VISIT_RESULT_ERROR;").ok();
1178    writeln!(out, "            }}").ok();
1179    writeln!(out, "        }};").ok();
1180    writeln!(out, "    }}").ok();
1181    writeln!(out).ok();
1182
1183    writeln!(out, "    @Override").ok();
1184    writeln!(out, "    public void close() {{").ok();
1185    writeln!(out, "        arena.close();").ok();
1186    writeln!(out, "    }}").ok();
1187    writeln!(out, "}}").ok();
1188    out
1189}
1190
1191// ---------------------------------------------------------------------------
1192// Internal helpers
1193// ---------------------------------------------------------------------------
1194
1195/// Generate camelCase stub variable name: stub + capitalize(java_method).
1196/// e.g. visitText -> stubVisitText
1197fn stub_var_name(java_method: &str) -> String {
1198    let mut name = String::with_capacity(5 + java_method.len());
1199    name.push_str("stub");
1200    let mut chars = java_method.chars();
1201    if let Some(first) = chars.next() {
1202        for c in first.to_uppercase() {
1203            name.push(c);
1204        }
1205        name.push_str(chars.as_str());
1206    }
1207    name
1208}
1209
1210fn handle_method_name(java_method: &str) -> String {
1211    // camelCase: "handle" + capitalize first letter of java_method
1212    let mut name = String::with_capacity(7 + java_method.len());
1213    name.push_str("handle");
1214    let mut chars = java_method.chars();
1215    if let Some(first) = chars.next() {
1216        for c in first.to_uppercase() {
1217            name.push(c);
1218        }
1219        name.push_str(chars.as_str());
1220    }
1221    name
1222}
1223
1224fn iface_param_str(spec: &CallbackSpec) -> String {
1225    let mut params = vec!["NodeContext context".to_string()];
1226    for ep in spec.extra {
1227        params.push(format!("{} {}", ep.java_type, ep.java_name));
1228    }
1229    if spec.has_is_header {
1230        params.push("boolean isHeader".to_string());
1231    }
1232    params.join(", ")
1233}
1234
1235/// Build the `FunctionDescriptor` for one callback's upcall stub.
1236/// All callbacks: (ADDRESS ctx, ADDRESS userData, ..extra.., ADDRESS outCustom, ADDRESS outLen) -> JAVA_INT
1237fn callback_descriptor(spec: &CallbackSpec) -> String {
1238    let mut layouts = vec![
1239        "ValueLayout.ADDRESS".to_string(), // ctx
1240        "ValueLayout.ADDRESS".to_string(), // user_data
1241    ];
1242    for ep in spec.extra {
1243        for layout in ep.c_layouts {
1244            layouts.push((*layout).to_string());
1245        }
1246    }
1247    if spec.has_is_header {
1248        layouts.push("ValueLayout.JAVA_INT".to_string());
1249    }
1250    layouts.push("ValueLayout.ADDRESS".to_string()); // out_custom
1251    layouts.push("ValueLayout.ADDRESS".to_string()); // out_len
1252    format!("FunctionDescriptor.of(ValueLayout.JAVA_INT, {})", layouts.join(", "))
1253}
1254
1255/// Build the `MethodType` for `LOOKUP.bind(this, name, type)`.
1256fn callback_method_type(spec: &CallbackSpec) -> String {
1257    let mut types = vec![
1258        "MemorySegment.class".to_string(), // ctx
1259        "MemorySegment.class".to_string(), // user_data
1260    ];
1261    for ep in spec.extra {
1262        for layout in ep.c_layouts {
1263            types.push(layout_to_java_class(layout).to_string());
1264        }
1265    }
1266    if spec.has_is_header {
1267        types.push("int.class".to_string());
1268    }
1269    types.push("MemorySegment.class".to_string()); // out_custom
1270    types.push("MemorySegment.class".to_string()); // out_len
1271    format!("MethodType.methodType(int.class, {})", types.join(", "))
1272}
1273
1274fn layout_to_java_class(layout: &str) -> &'static str {
1275    match layout {
1276        "ValueLayout.ADDRESS" => "MemorySegment.class",
1277        "ValueLayout.JAVA_INT" => "int.class",
1278        "ValueLayout.JAVA_LONG" => "long.class",
1279        _ => "long.class",
1280    }
1281}
1282
1283/// Generate one `handle_*` instance method inside `VisitorBridge`.
1284fn gen_handle_method(out: &mut String, spec: &CallbackSpec) {
1285    // Build method signature matching the MethodType passed to upcallStub.
1286    let mut params = vec!["MemorySegment ctx".to_string(), "MemorySegment userData".to_string()];
1287    for ep in spec.extra {
1288        for (c_idx, layout) in ep.c_layouts.iter().enumerate() {
1289            let java_ptype = match *layout {
1290                "ValueLayout.JAVA_INT" => "int",
1291                "ValueLayout.JAVA_LONG" => "long",
1292                _ => "MemorySegment",
1293            };
1294            params.push(format!("{java_ptype} {}", raw_var_name(ep.java_name, c_idx)));
1295        }
1296    }
1297    if spec.has_is_header {
1298        params.push("int isHeader".to_string());
1299    }
1300    params.push("MemorySegment outCustom".to_string());
1301    params.push("MemorySegment outLen".to_string());
1302
1303    writeln!(
1304        out,
1305        "    int {}({}) {{",
1306        handle_method_name(spec.java_method),
1307        params.join(", ")
1308    )
1309    .ok();
1310    writeln!(out, "        try (var encArena = Arena.ofConfined()) {{").ok();
1311    writeln!(out, "            var context = decodeNodeContext(ctx);").ok();
1312
1313    // Decode each extra param
1314    for ep in spec.extra {
1315        let mut decode = ep.decode.to_string();
1316        for (c_idx, _) in ep.c_layouts.iter().enumerate() {
1317            let placeholder = format!("raw_{}_{}", ep.java_name, c_idx);
1318            let var = raw_var_name(ep.java_name, c_idx);
1319            decode = decode.replace(&placeholder, &var);
1320        }
1321        writeln!(out, "            var {} = {};", ep.java_name, decode).ok();
1322    }
1323    if spec.has_is_header {
1324        writeln!(out, "            var goIsHeader = isHeader != 0;").ok();
1325    }
1326
1327    // Build call args
1328    let mut call_args = vec!["context".to_string()];
1329    for ep in spec.extra {
1330        call_args.push(ep.java_name.to_string());
1331    }
1332    if spec.has_is_header {
1333        call_args.push("goIsHeader".to_string());
1334    }
1335
1336    writeln!(
1337        out,
1338        "            var result = visitor.{}({});",
1339        spec.java_method,
1340        call_args.join(", ")
1341    )
1342    .ok();
1343    writeln!(
1344        out,
1345        "            return encodeVisitResult(result, outCustom, outLen, encArena);"
1346    )
1347    .ok();
1348    writeln!(out, "        }} catch (Throwable ignored) {{").ok();
1349    writeln!(out, "            return 0;").ok();
1350    writeln!(out, "        }}").ok();
1351    writeln!(out, "    }}").ok();
1352    writeln!(out).ok();
1353}
1354
1355fn raw_var_name(java_name: &str, c_idx: usize) -> String {
1356    // camelCase: "raw" + capitalize first letter of java_name + "_" + index
1357    // e.g. raw_text_0 -> rawText0, raw_cells_1 -> rawCells1
1358    let mut name = String::with_capacity(4 + java_name.len() + 2);
1359    name.push_str("raw");
1360    let mut chars = java_name.chars();
1361    if let Some(first) = chars.next() {
1362        for c in first.to_uppercase() {
1363            name.push(c);
1364        }
1365        name.push_str(chars.as_str());
1366    }
1367    name.push_str(&c_idx.to_string());
1368    name
1369}