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