Skip to main content

rajac_bytecode/
pretty_print.rs

1use rajac_base::shared_string::SharedString;
2use ristretto_classfile::attributes::Attribute;
3use ristretto_classfile::{ClassFile, ConstantPool, Field, Method};
4
5pub fn pretty_print_classfile(class_file: &ClassFile) -> SharedString {
6    let mut out = String::new();
7
8    let class_name_internal = class_file
9        .constant_pool
10        .try_get_class(class_file.this_class)
11        .unwrap_or("<invalid:this_class>");
12    let super_name_internal = class_file
13        .constant_pool
14        .try_get_class(class_file.super_class)
15        .unwrap_or("<invalid:super_class>");
16
17    let class_name = internal_to_java_name(class_name_internal);
18    let super_name = internal_to_java_name(super_name_internal);
19
20    out.push_str(&format!(
21        "// version: {}.{} ({})\n",
22        class_file.version.major(),
23        class_file.version.minor(),
24        class_file.version
25    ));
26
27    out.push_str(&class_file.access_flags.as_code());
28    out.push(' ');
29    out.push_str(&class_name);
30    if super_name_internal != "java/lang/Object" {
31        out.push_str(" extends ");
32        out.push_str(&super_name);
33    }
34    out.push_str(" {\n");
35
36    if !class_file.interfaces.is_empty() {
37        out.push_str("  // implements\n");
38        for iface in &class_file.interfaces {
39            let iface_name = class_file
40                .constant_pool
41                .try_get_class(*iface)
42                .map(internal_to_java_name)
43                .unwrap_or_else(|_| "<invalid:interface>".to_string());
44            out.push_str(&format!("  // - {}\n", iface_name));
45        }
46    }
47
48    if !class_file.fields.is_empty() {
49        out.push_str("\n  // fields\n");
50        let mut sorted_fields: Vec<_> = class_file.fields.iter().collect();
51        sorted_fields.sort_by(|a, b| {
52            let name_a = class_file
53                .constant_pool
54                .try_get_utf8(a.name_index)
55                .unwrap_or("");
56            let name_b = class_file
57                .constant_pool
58                .try_get_utf8(b.name_index)
59                .unwrap_or("");
60            name_a.cmp(name_b)
61        });
62        for field in sorted_fields {
63            pretty_print_field(&mut out, &class_file.constant_pool, field);
64        }
65    }
66
67    out.push_str("\n  // methods\n");
68    // Sort methods alphabetically by name for consistent output
69    let mut sorted_methods: Vec<_> = class_file.methods.iter().collect();
70    sorted_methods.sort_by(|a, b| {
71        let name_a = class_file
72            .constant_pool
73            .try_get_utf8(a.name_index)
74            .unwrap_or("");
75        let name_b = class_file
76            .constant_pool
77            .try_get_utf8(b.name_index)
78            .unwrap_or("");
79        name_a.cmp(name_b)
80    });
81    for method in sorted_methods {
82        pretty_print_method(&mut out, &class_file.constant_pool, method);
83    }
84
85    if !class_file.attributes.is_empty() {
86        out.push_str("\n  // class attributes\n");
87        let mut rendered_attributes = class_file
88            .attributes
89            .iter()
90            .filter_map(|attribute| render_class_attribute(attribute, &class_file.constant_pool))
91            .collect::<Vec<_>>();
92        rendered_attributes.sort();
93        for rendered in rendered_attributes {
94            out.push_str(&rendered);
95        }
96    }
97
98    out.push_str("}\n");
99
100    SharedString::from(out)
101}
102
103fn internal_to_java_name(internal: &str) -> String {
104    internal.replace('/', ".")
105}
106
107fn resolve_class_name(constant_pool: &ConstantPool, index: u16) -> String {
108    constant_pool
109        .try_get_class(index)
110        .map(internal_to_java_name)
111        .unwrap_or_else(|_| "<invalid:class>".to_string())
112}
113
114fn resolve_optional_class_name(
115    constant_pool: &ConstantPool,
116    index: u16,
117    empty_value: &str,
118) -> String {
119    if index == 0 {
120        return empty_value.to_string();
121    }
122    resolve_class_name(constant_pool, index)
123}
124
125fn resolve_optional_utf8(constant_pool: &ConstantPool, index: u16, empty_value: &str) -> String {
126    if index == 0 {
127        return empty_value.to_string();
128    }
129    constant_pool
130        .try_get_utf8(index)
131        .map(|value| value.to_string())
132        .unwrap_or_else(|_| "<invalid:utf8>".to_string())
133}
134
135/* 📖 # Why normalize constant-pool-backed instruction rendering?
136Verification should compare symbolic classfile content rather than incidental constant-pool slot
137allocation. Pretty-printed instructions therefore resolve constant-pool references to their
138targets, values, or symbols and avoid exposing pool indexes in the output whenever possible.
139*/
140fn format_ldc_constant(constant_pool: &ConstantPool, index: u16) -> String {
141    if let Ok(value) = constant_pool.try_get_utf8(index) {
142        return format!("ldc \"{value}\"");
143    }
144
145    if let Ok(value) = constant_pool.try_get_string(index) {
146        return format!("ldc \"{value}\"");
147    }
148
149    if let Ok(value) = constant_pool.try_get_integer(index) {
150        return format!("ldc {value}");
151    }
152
153    if let Ok(value) = constant_pool.try_get_float(index) {
154        return format!("ldc {value}");
155    }
156
157    if let Ok(value) = constant_pool.try_get_long(index) {
158        return format!("ldc {value}");
159    }
160
161    if let Ok(value) = constant_pool.try_get_double(index) {
162        return format!("ldc {value}");
163    }
164
165    "ldc <invalid:constant>".to_string()
166}
167
168fn pretty_print_field(out: &mut String, constant_pool: &ConstantPool, field: &Field) {
169    let name = constant_pool
170        .try_get_utf8(field.name_index)
171        .unwrap_or("<invalid:name>");
172    let descriptor = constant_pool
173        .try_get_utf8(field.descriptor_index)
174        .unwrap_or("<invalid:descriptor>");
175
176    out.push_str(&format!(
177        "  {} {} /* {} */;\n",
178        field.access_flags.as_code(),
179        name,
180        descriptor
181    ));
182}
183
184fn pretty_print_method(out: &mut String, constant_pool: &ConstantPool, method: &Method) {
185    let name = constant_pool
186        .try_get_utf8(method.name_index)
187        .unwrap_or("<invalid:name>");
188    let descriptor = constant_pool
189        .try_get_utf8(method.descriptor_index)
190        .unwrap_or("<invalid:descriptor>");
191
192    out.push_str(&format!(
193        "  {}{}{} /* {} */;\n",
194        method.access_flags.as_code(),
195        if name == "<init>" { "" } else { " " },
196        name,
197        descriptor
198    ));
199
200    // Print bytecode if Code attribute is present
201    for attribute in &method.attributes {
202        if let Attribute::Code {
203            max_stack,
204            max_locals,
205            code,
206            exception_table,
207            ..
208        } = attribute
209        {
210            out.push_str("    Code:\n");
211            out.push_str(&format!("     max_stack = {}\n", max_stack));
212            out.push_str(&format!("     max_locals = {}\n", max_locals));
213
214            if !code.is_empty() {
215                out.push_str("     Code:\n");
216                for (i, instruction) in code.iter().enumerate() {
217                    let offset = i;
218                    let instruction_str = format_instruction(instruction, constant_pool);
219                    out.push_str(&format!("      {}: {}\n", offset, instruction_str));
220                }
221            }
222
223            if !exception_table.is_empty() {
224                out.push_str("     ExceptionTable:\n");
225                for (i, exception) in exception_table.iter().enumerate() {
226                    out.push_str(&format!(
227                        "      {} {} {} {} {}\n",
228                        i,
229                        exception.range_pc.start,
230                        exception.range_pc.end,
231                        exception.handler_pc,
232                        exception.catch_type
233                    ));
234                }
235            }
236
237            // Print Code attributes (like LineNumberTable, LocalVariableTable)
238            // This is a placeholder - we'd need to handle Code sub-attributes
239            // but the current structure doesn't expose them directly
240        }
241        if let Attribute::Exceptions {
242            exception_indexes, ..
243        } = attribute
244        {
245            out.push_str("    Exceptions:\n");
246            for exception_index in exception_indexes {
247                let exception_name = constant_pool
248                    .try_get_class(*exception_index)
249                    .unwrap_or("<invalid:class>");
250                out.push_str(&format!("     {}\n", exception_name));
251            }
252        }
253    }
254}
255
256fn format_instruction(
257    instruction: &ristretto_classfile::attributes::Instruction,
258    constant_pool: &ConstantPool,
259) -> String {
260    use ristretto_classfile::attributes::Instruction;
261
262    match instruction {
263        Instruction::Nop => "nop".to_string(),
264        Instruction::Aconst_null => "aconst_null".to_string(),
265        Instruction::Iconst_m1 => "iconst_m1".to_string(),
266        Instruction::Iconst_0 => "iconst_0".to_string(),
267        Instruction::Iconst_1 => "iconst_1".to_string(),
268        Instruction::Iconst_2 => "iconst_2".to_string(),
269        Instruction::Iconst_3 => "iconst_3".to_string(),
270        Instruction::Iconst_4 => "iconst_4".to_string(),
271        Instruction::Iconst_5 => "iconst_5".to_string(),
272        Instruction::Lconst_0 => "lconst_0".to_string(),
273        Instruction::Lconst_1 => "lconst_1".to_string(),
274        Instruction::Fconst_0 => "fconst_0".to_string(),
275        Instruction::Fconst_1 => "fconst_1".to_string(),
276        Instruction::Fconst_2 => "fconst_2".to_string(),
277        Instruction::Dconst_0 => "dconst_0".to_string(),
278        Instruction::Dconst_1 => "dconst_1".to_string(),
279        Instruction::Bipush(byte) => format!("bipush {}", byte),
280        Instruction::Sipush(short) => format!("sipush {}", short),
281        Instruction::Ldc(index) => format_ldc_constant(constant_pool, u16::from(*index)),
282        Instruction::Ldc_w(index) => format_ldc_constant(constant_pool, *index),
283        Instruction::Ldc2_w(index) => format_ldc_constant(constant_pool, *index),
284        Instruction::Iload(index) => format!("iload {}", index),
285        Instruction::Lload(index) => format!("lload {}", index),
286        Instruction::Fload(index) => format!("fload {}", index),
287        Instruction::Dload(index) => format!("dload {}", index),
288        Instruction::Aload(index) => format!("aload {}", index),
289        Instruction::Iload_0 => "iload_0".to_string(),
290        Instruction::Iload_1 => "iload_1".to_string(),
291        Instruction::Iload_2 => "iload_2".to_string(),
292        Instruction::Iload_3 => "iload_3".to_string(),
293        Instruction::Lload_0 => "lload_0".to_string(),
294        Instruction::Lload_1 => "lload_1".to_string(),
295        Instruction::Lload_2 => "lload_2".to_string(),
296        Instruction::Lload_3 => "lload_3".to_string(),
297        Instruction::Fload_0 => "fload_0".to_string(),
298        Instruction::Fload_1 => "fload_1".to_string(),
299        Instruction::Fload_2 => "fload_2".to_string(),
300        Instruction::Fload_3 => "fload_3".to_string(),
301        Instruction::Dload_0 => "dload_0".to_string(),
302        Instruction::Dload_1 => "dload_1".to_string(),
303        Instruction::Dload_2 => "dload_2".to_string(),
304        Instruction::Dload_3 => "dload_3".to_string(),
305        Instruction::Aload_0 => "aload_0".to_string(),
306        Instruction::Aload_1 => "aload_1".to_string(),
307        Instruction::Aload_2 => "aload_2".to_string(),
308        Instruction::Aload_3 => "aload_3".to_string(),
309        Instruction::Iaload => "iaload".to_string(),
310        Instruction::Laload => "laload".to_string(),
311        Instruction::Faload => "faload".to_string(),
312        Instruction::Daload => "daload".to_string(),
313        Instruction::Aaload => "aaload".to_string(),
314        Instruction::Baload => "baload".to_string(),
315        Instruction::Caload => "caload".to_string(),
316        Instruction::Saload => "saload".to_string(),
317        Instruction::Istore(index) => format!("istore {}", index),
318        Instruction::Lstore(index) => format!("lstore {}", index),
319        Instruction::Fstore(index) => format!("fstore {}", index),
320        Instruction::Dstore(index) => format!("dstore {}", index),
321        Instruction::Astore(index) => format!("astore {}", index),
322        Instruction::Istore_0 => "istore_0".to_string(),
323        Instruction::Istore_1 => "istore_1".to_string(),
324        Instruction::Istore_2 => "istore_2".to_string(),
325        Instruction::Istore_3 => "istore_3".to_string(),
326        Instruction::Lstore_0 => "lstore_0".to_string(),
327        Instruction::Lstore_1 => "lstore_1".to_string(),
328        Instruction::Lstore_2 => "lstore_2".to_string(),
329        Instruction::Lstore_3 => "lstore_3".to_string(),
330        Instruction::Fstore_0 => "fstore_0".to_string(),
331        Instruction::Fstore_1 => "fstore_1".to_string(),
332        Instruction::Fstore_2 => "fstore_2".to_string(),
333        Instruction::Fstore_3 => "fstore_3".to_string(),
334        Instruction::Dstore_0 => "dstore_0".to_string(),
335        Instruction::Dstore_1 => "dstore_1".to_string(),
336        Instruction::Dstore_2 => "dstore_2".to_string(),
337        Instruction::Dstore_3 => "dstore_3".to_string(),
338        Instruction::Astore_0 => "astore_0".to_string(),
339        Instruction::Astore_1 => "astore_1".to_string(),
340        Instruction::Astore_2 => "astore_2".to_string(),
341        Instruction::Astore_3 => "astore_3".to_string(),
342        Instruction::Iastore => "iastore".to_string(),
343        Instruction::Lastore => "lastore".to_string(),
344        Instruction::Fastore => "fastore".to_string(),
345        Instruction::Dastore => "dastore".to_string(),
346        Instruction::Aastore => "aastore".to_string(),
347        Instruction::Bastore => "bastore".to_string(),
348        Instruction::Castore => "castore".to_string(),
349        Instruction::Sastore => "sastore".to_string(),
350        Instruction::Pop => "pop".to_string(),
351        Instruction::Pop2 => "pop2".to_string(),
352        Instruction::Dup => "dup".to_string(),
353        Instruction::Dup_x1 => "dup_x1".to_string(),
354        Instruction::Dup_x2 => "dup_x2".to_string(),
355        Instruction::Dup2 => "dup2".to_string(),
356        Instruction::Dup2_x1 => "dup2_x1".to_string(),
357        Instruction::Dup2_x2 => "dup2_x2".to_string(),
358        Instruction::Swap => "swap".to_string(),
359        Instruction::Iadd => "iadd".to_string(),
360        Instruction::Ladd => "ladd".to_string(),
361        Instruction::Fadd => "fadd".to_string(),
362        Instruction::Dadd => "dadd".to_string(),
363        Instruction::Isub => "isub".to_string(),
364        Instruction::Lsub => "lsub".to_string(),
365        Instruction::Fsub => "fsub".to_string(),
366        Instruction::Dsub => "dsub".to_string(),
367        Instruction::Imul => "imul".to_string(),
368        Instruction::Lmul => "lmul".to_string(),
369        Instruction::Fmul => "fmul".to_string(),
370        Instruction::Dmul => "dmul".to_string(),
371        Instruction::Idiv => "idiv".to_string(),
372        Instruction::Ldiv => "ldiv".to_string(),
373        Instruction::Fdiv => "fdiv".to_string(),
374        Instruction::Ddiv => "ddiv".to_string(),
375        Instruction::Irem => "irem".to_string(),
376        Instruction::Lrem => "lrem".to_string(),
377        Instruction::Frem => "frem".to_string(),
378        Instruction::Drem => "drem".to_string(),
379        Instruction::Ineg => "ineg".to_string(),
380        Instruction::Lneg => "lneg".to_string(),
381        Instruction::Fneg => "fneg".to_string(),
382        Instruction::Dneg => "dneg".to_string(),
383        Instruction::Ishl => "ishl".to_string(),
384        Instruction::Lshl => "lshl".to_string(),
385        Instruction::Ishr => "ishr".to_string(),
386        Instruction::Lshr => "lshr".to_string(),
387        Instruction::Iushr => "iushr".to_string(),
388        Instruction::Lushr => "lushr".to_string(),
389        Instruction::Iand => "iand".to_string(),
390        Instruction::Land => "land".to_string(),
391        Instruction::Ior => "ior".to_string(),
392        Instruction::Lor => "lor".to_string(),
393        Instruction::Ixor => "ixor".to_string(),
394        Instruction::Lxor => "lxor".to_string(),
395        Instruction::Iinc(index, value) => format!("iinc {} {}", index, value),
396        Instruction::I2l => "i2l".to_string(),
397        Instruction::I2f => "i2f".to_string(),
398        Instruction::I2d => "i2d".to_string(),
399        Instruction::L2i => "l2i".to_string(),
400        Instruction::L2f => "l2f".to_string(),
401        Instruction::L2d => "l2d".to_string(),
402        Instruction::F2i => "f2i".to_string(),
403        Instruction::F2l => "f2l".to_string(),
404        Instruction::F2d => "f2d".to_string(),
405        Instruction::D2i => "d2i".to_string(),
406        Instruction::D2l => "d2l".to_string(),
407        Instruction::D2f => "d2f".to_string(),
408        Instruction::I2b => "i2b".to_string(),
409        Instruction::I2c => "i2c".to_string(),
410        Instruction::I2s => "i2s".to_string(),
411        Instruction::Lcmp => "lcmp".to_string(),
412        Instruction::Fcmpl => "fcmpl".to_string(),
413        Instruction::Fcmpg => "fcmpg".to_string(),
414        Instruction::Dcmpl => "dcmpl".to_string(),
415        Instruction::Dcmpg => "dcmpg".to_string(),
416        Instruction::Ifeq(branch) => format!("ifeq {}", branch),
417        Instruction::Ifne(branch) => format!("ifne {}", branch),
418        Instruction::Iflt(branch) => format!("iflt {}", branch),
419        Instruction::Ifge(branch) => format!("ifge {}", branch),
420        Instruction::Ifgt(branch) => format!("ifgt {}", branch),
421        Instruction::Ifle(branch) => format!("ifle {}", branch),
422        Instruction::If_icmpeq(branch) => format!("if_icmpeq {}", branch),
423        Instruction::If_icmpne(branch) => format!("if_icmpne {}", branch),
424        Instruction::If_icmplt(branch) => format!("if_icmplt {}", branch),
425        Instruction::If_icmpge(branch) => format!("if_icmpge {}", branch),
426        Instruction::If_icmpgt(branch) => format!("if_icmpgt {}", branch),
427        Instruction::If_icmple(branch) => format!("if_icmple {}", branch),
428        Instruction::If_acmpeq(branch) => format!("if_acmpeq {}", branch),
429        Instruction::If_acmpne(branch) => format!("if_acmpne {}", branch),
430        Instruction::Goto(branch) => format!("goto {}", branch),
431        Instruction::Jsr(branch) => format!("jsr {}", branch),
432        Instruction::Ret(index) => format!("ret {}", index),
433        Instruction::Tableswitch(table_switch) => {
434            format!(
435                "tableswitch {{ {} to {} }}",
436                table_switch.low, table_switch.high
437            )
438        }
439        Instruction::Lookupswitch(lookup_switch) => {
440            format!("lookupswitch {{ {} entries }}", lookup_switch.pairs.len())
441        }
442        Instruction::Ireturn => "ireturn".to_string(),
443        Instruction::Lreturn => "lreturn".to_string(),
444        Instruction::Freturn => "freturn".to_string(),
445        Instruction::Dreturn => "dreturn".to_string(),
446        Instruction::Areturn => "areturn".to_string(),
447        Instruction::Return => "return".to_string(),
448        Instruction::Getstatic(index) => match constant_pool.try_get_field_ref(*index) {
449            Ok((class_index, name_and_type_index)) => {
450                let class = constant_pool
451                    .try_get_class(*class_index)
452                    .unwrap_or("<invalid:class>");
453                let (name_index, descriptor_index) = constant_pool
454                    .try_get_name_and_type(*name_and_type_index)
455                    .unwrap_or((&0, &0));
456                let name = constant_pool
457                    .try_get_utf8(*name_index)
458                    .unwrap_or("<invalid:name>");
459                let descriptor = constant_pool
460                    .try_get_utf8(*descriptor_index)
461                    .unwrap_or("<invalid:descriptor>");
462                format!(
463                    "getstatic {}.{}:{}",
464                    internal_to_java_name(class),
465                    name,
466                    descriptor
467                )
468            }
469            Err(_) => format!("getstatic #{}", index),
470        },
471        Instruction::Putstatic(index) => match constant_pool.try_get_field_ref(*index) {
472            Ok((class_index, name_and_type_index)) => {
473                let class = constant_pool
474                    .try_get_class(*class_index)
475                    .unwrap_or("<invalid:class>");
476                let (name_index, descriptor_index) = constant_pool
477                    .try_get_name_and_type(*name_and_type_index)
478                    .unwrap_or((&0, &0));
479                let name = constant_pool
480                    .try_get_utf8(*name_index)
481                    .unwrap_or("<invalid:name>");
482                let descriptor = constant_pool
483                    .try_get_utf8(*descriptor_index)
484                    .unwrap_or("<invalid:descriptor>");
485                format!(
486                    "putstatic {}.{}:{}",
487                    internal_to_java_name(class),
488                    name,
489                    descriptor
490                )
491            }
492            Err(_) => format!("putstatic #{}", index),
493        },
494        Instruction::Getfield(index) => match constant_pool.try_get_field_ref(*index) {
495            Ok((class_index, name_and_type_index)) => {
496                let class = constant_pool
497                    .try_get_class(*class_index)
498                    .unwrap_or("<invalid:class>");
499                let (name_index, descriptor_index) = constant_pool
500                    .try_get_name_and_type(*name_and_type_index)
501                    .unwrap_or((&0, &0));
502                let name = constant_pool
503                    .try_get_utf8(*name_index)
504                    .unwrap_or("<invalid:name>");
505                let descriptor = constant_pool
506                    .try_get_utf8(*descriptor_index)
507                    .unwrap_or("<invalid:descriptor>");
508                format!(
509                    "getfield {}.{}:{}",
510                    internal_to_java_name(class),
511                    name,
512                    descriptor
513                )
514            }
515            Err(_) => format!("getfield #{}", index),
516        },
517        Instruction::Putfield(index) => match constant_pool.try_get_field_ref(*index) {
518            Ok((class_index, name_and_type_index)) => {
519                let class = constant_pool
520                    .try_get_class(*class_index)
521                    .unwrap_or("<invalid:class>");
522                let (name_index, descriptor_index) = constant_pool
523                    .try_get_name_and_type(*name_and_type_index)
524                    .unwrap_or((&0, &0));
525                let name = constant_pool
526                    .try_get_utf8(*name_index)
527                    .unwrap_or("<invalid:name>");
528                let descriptor = constant_pool
529                    .try_get_utf8(*descriptor_index)
530                    .unwrap_or("<invalid:descriptor>");
531                format!(
532                    "putfield {}.{}:{}",
533                    internal_to_java_name(class),
534                    name,
535                    descriptor
536                )
537            }
538            Err(_) => format!("putfield #{}", index),
539        },
540        Instruction::Invokevirtual(index) => match constant_pool.try_get_method_ref(*index) {
541            Ok((class_index, name_and_type_index)) => {
542                let class = constant_pool
543                    .try_get_class(*class_index)
544                    .unwrap_or("<invalid:class>");
545                let (name_index, descriptor_index) = constant_pool
546                    .try_get_name_and_type(*name_and_type_index)
547                    .unwrap_or((&0, &0));
548                let name = constant_pool
549                    .try_get_utf8(*name_index)
550                    .unwrap_or("<invalid:name>");
551                let descriptor = constant_pool
552                    .try_get_utf8(*descriptor_index)
553                    .unwrap_or("<invalid:descriptor>");
554                format!(
555                    "invokevirtual {}.{}:{}",
556                    internal_to_java_name(class),
557                    name,
558                    descriptor
559                )
560            }
561            Err(_) => format!("invokevirtual #{}", index),
562        },
563        Instruction::Invokespecial(index) => match constant_pool.try_get_method_ref(*index) {
564            Ok((class_index, name_and_type_index)) => {
565                let class = constant_pool
566                    .try_get_class(*class_index)
567                    .unwrap_or("<invalid:class>");
568                let (name_index, descriptor_index) = constant_pool
569                    .try_get_name_and_type(*name_and_type_index)
570                    .unwrap_or((&0, &0));
571                let name = constant_pool
572                    .try_get_utf8(*name_index)
573                    .unwrap_or("<invalid:name>");
574                let descriptor = constant_pool
575                    .try_get_utf8(*descriptor_index)
576                    .unwrap_or("<invalid:descriptor>");
577                format!(
578                    "invokespecial {}.{}:{}",
579                    internal_to_java_name(class),
580                    name,
581                    descriptor
582                )
583            }
584            Err(_) => format!("invokespecial #{}", index),
585        },
586        Instruction::Invokestatic(index) => match constant_pool.try_get_method_ref(*index) {
587            Ok((class_index, name_and_type_index)) => {
588                let class = constant_pool
589                    .try_get_class(*class_index)
590                    .unwrap_or("<invalid:class>");
591                let (name_index, descriptor_index) = constant_pool
592                    .try_get_name_and_type(*name_and_type_index)
593                    .unwrap_or((&0, &0));
594                let name = constant_pool
595                    .try_get_utf8(*name_index)
596                    .unwrap_or("<invalid:name>");
597                let descriptor = constant_pool
598                    .try_get_utf8(*descriptor_index)
599                    .unwrap_or("<invalid:descriptor>");
600                format!(
601                    "invokestatic {}.{}:{}",
602                    internal_to_java_name(class),
603                    name,
604                    descriptor
605                )
606            }
607            Err(_) => format!("invokestatic #{}", index),
608        },
609        Instruction::Invokeinterface(index, count) => {
610            match constant_pool.try_get_interface_method_ref(*index) {
611                Ok((class_index, name_and_type_index)) => {
612                    let class = constant_pool
613                        .try_get_class(*class_index)
614                        .unwrap_or("<invalid:class>");
615                    let (name_index, descriptor_index) = constant_pool
616                        .try_get_name_and_type(*name_and_type_index)
617                        .unwrap_or((&0, &0));
618                    let name = constant_pool
619                        .try_get_utf8(*name_index)
620                        .unwrap_or("<invalid:name>");
621                    let descriptor = constant_pool
622                        .try_get_utf8(*descriptor_index)
623                        .unwrap_or("<invalid:descriptor>");
624                    format!(
625                        "invokeinterface {}.{}:{} {}",
626                        internal_to_java_name(class),
627                        name,
628                        descriptor,
629                        count
630                    )
631                }
632                Err(_) => format!("invokeinterface #{} {}", index, count),
633            }
634        }
635        Instruction::Invokedynamic(index) => match constant_pool.try_get_invoke_dynamic(*index) {
636            Ok((name, descriptor)) => {
637                format!("invokedynamic {}:{}", name, descriptor)
638            }
639            Err(_) => format!("invokedynamic #{}", index),
640        },
641        Instruction::New(index) => match constant_pool.try_get_class(*index) {
642            Ok(class) => {
643                format!("new {}", internal_to_java_name(class))
644            }
645            Err(_) => format!("new #{}", index),
646        },
647        Instruction::Newarray(array_type) => {
648            let type_name = match array_type {
649                ristretto_classfile::attributes::ArrayType::Boolean => "boolean",
650                ristretto_classfile::attributes::ArrayType::Char => "char",
651                ristretto_classfile::attributes::ArrayType::Float => "float",
652                ristretto_classfile::attributes::ArrayType::Double => "double",
653                ristretto_classfile::attributes::ArrayType::Byte => "byte",
654                ristretto_classfile::attributes::ArrayType::Short => "short",
655                ristretto_classfile::attributes::ArrayType::Int => "int",
656                ristretto_classfile::attributes::ArrayType::Long => "long",
657            };
658            format!("newarray {}", type_name)
659        }
660        Instruction::Anewarray(index) => match constant_pool.try_get_class(*index) {
661            Ok(class) => {
662                format!("anewarray {}", internal_to_java_name(class))
663            }
664            Err(_) => format!("anewarray #{}", index),
665        },
666        Instruction::Arraylength => "arraylength".to_string(),
667        Instruction::Athrow => "athrow".to_string(),
668        Instruction::Checkcast(index) => match constant_pool.try_get_class(*index) {
669            Ok(class) => {
670                format!("checkcast {}", internal_to_java_name(class))
671            }
672            Err(_) => format!("checkcast #{}", index),
673        },
674        Instruction::Instanceof(index) => match constant_pool.try_get_class(*index) {
675            Ok(class) => {
676                format!("instanceof {}", internal_to_java_name(class))
677            }
678            Err(_) => format!("instanceof #{}", index),
679        },
680        Instruction::Monitorenter => "monitorenter".to_string(),
681        Instruction::Monitorexit => "monitorexit".to_string(),
682        Instruction::Wide => "wide".to_string(),
683        Instruction::Multianewarray(index, dimensions) => {
684            match constant_pool.try_get_class(*index) {
685                Ok(class) => {
686                    format!(
687                        "multianewarray {} {}",
688                        internal_to_java_name(class),
689                        dimensions
690                    )
691                }
692                Err(_) => format!("multianewarray #{} {}", index, dimensions),
693            }
694        }
695        Instruction::Ifnull(branch) => format!("ifnull {}", branch),
696        Instruction::Ifnonnull(branch) => format!("ifnonnull {}", branch),
697        Instruction::Goto_w(branch) => format!("goto_w {}", branch),
698        Instruction::Jsr_w(branch) => format!("jsr_w {}", branch),
699        Instruction::Breakpoint => "breakpoint".to_string(),
700        Instruction::Impdep1 => "impdep1".to_string(),
701        Instruction::Impdep2 => "impdep2".to_string(),
702        _ => format!("unknown instruction: {:?}", instruction),
703    }
704}
705
706fn render_class_attribute(attribute: &Attribute, constant_pool: &ConstantPool) -> Option<String> {
707    match attribute {
708        Attribute::SourceFile {
709            source_file_index, ..
710        } => {
711            let source_file_name = constant_pool
712                .try_get_utf8(*source_file_index)
713                .unwrap_or("<invalid:source_file>");
714            Some(format!("  // SourceFile: {}\n", source_file_name))
715        }
716        Attribute::InnerClasses { classes, .. } => {
717            let mut rendered = String::from("  // InnerClasses:\n");
718            let mut entries = classes
719                .iter()
720                .map(|entry| {
721                    let inner_name = resolve_class_name(constant_pool, entry.class_info_index);
722                    let outer_name = resolve_optional_class_name(
723                        constant_pool,
724                        entry.outer_class_info_index,
725                        "<none>",
726                    );
727                    let inner_simple =
728                        resolve_optional_utf8(constant_pool, entry.name_index, "<anonymous>");
729                    format!(
730                        "  // - inner: {} outer: {} name: {} flags: {}\n",
731                        inner_name, outer_name, inner_simple, entry.access_flags
732                    )
733                })
734                .collect::<Vec<_>>();
735            entries.sort();
736            for entry in entries {
737                rendered.push_str(&entry);
738            }
739            Some(rendered)
740        }
741        Attribute::NestHost {
742            host_class_index, ..
743        } => {
744            let host_name = resolve_class_name(constant_pool, *host_class_index);
745            Some(format!("  // NestHost: {}\n", host_name))
746        }
747        Attribute::NestMembers { class_indexes, .. } => {
748            let mut rendered = String::from("  // NestMembers:\n");
749            let mut members = class_indexes
750                .iter()
751                .map(|class_index| {
752                    format!(
753                        "  // - {}\n",
754                        resolve_class_name(constant_pool, *class_index)
755                    )
756                })
757                .collect::<Vec<_>>();
758            members.sort();
759            for member in members {
760                rendered.push_str(&member);
761            }
762            Some(rendered)
763        }
764        // TODO: Re-enable metadata-oriented attributes like Signature once rajac emits the same
765        // verification-relevant metadata coverage as the OpenJDK reference output.
766        Attribute::Signature { .. } => None,
767        _ => None,
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774    use expect_test::expect;
775    use rajac_ast::{
776        Ast, AstArena, AstType, ClassDecl, ClassKind, ClassMember, Field, Method, Modifiers,
777        PrimitiveType,
778    };
779    use rajac_symbols::SymbolTable;
780    use rajac_types::Ident;
781    use ristretto_classfile::attributes::{InnerClass, NestedClassAccessFlags};
782    use ristretto_classfile::{ClassAccessFlags, ConstantPool, JAVA_21};
783
784    #[test]
785    fn pretty_print_is_java_like_and_includes_details() {
786        let mut arena = AstArena::new();
787        let mut ast = Ast::new(SharedString::new("test"));
788        let type_arena = rajac_types::TypeArena::new();
789        let symbol_table = SymbolTable::new();
790
791        let int_ty = arena.alloc_type(AstType::Primitive {
792            kind: PrimitiveType::Int,
793            ty: rajac_types::TypeId::INVALID,
794        });
795        let void_ty = arena.alloc_type(AstType::Primitive {
796            kind: PrimitiveType::Void,
797            ty: rajac_types::TypeId::INVALID,
798        });
799
800        let field = Field {
801            name: Ident::new(SharedString::new("x")),
802            ty: int_ty,
803            initializer: None,
804            modifiers: Modifiers(Modifiers::PUBLIC | Modifiers::STATIC | Modifiers::FINAL),
805        };
806        let method = Method {
807            name: Ident::new(SharedString::new("f")),
808            params: vec![],
809            return_ty: void_ty,
810            body: None,
811            throws: vec![],
812            modifiers: Modifiers(Modifiers::PUBLIC),
813        };
814
815        let field_member_id = arena.alloc_class_member(ClassMember::Field(field));
816        let method_member_id = arena.alloc_class_member(ClassMember::Method(method));
817        let class_id = arena.alloc_class_decl(ClassDecl {
818            kind: ClassKind::Interface,
819            name: Ident::new(SharedString::new("Foo")),
820            type_params: vec![],
821            extends: None,
822            implements: vec![],
823            permits: vec![],
824            enum_entries: vec![],
825            members: vec![field_member_id, method_member_id],
826            modifiers: Modifiers(Modifiers::PUBLIC),
827        });
828        ast.classes.push(class_id);
829
830        let class_file = crate::classfile::classfile_from_class_decl(
831            &ast,
832            &arena,
833            class_id,
834            &type_arena,
835            &symbol_table,
836        )
837        .unwrap();
838        class_file.verify().unwrap();
839
840        let printed = pretty_print_classfile(&class_file);
841        let printed = printed.as_str();
842
843        expect![[r#"
844            // version: 65.0 (Java 21)
845            public abstract interface Foo {
846
847              // fields
848              public static final x /* I */;
849
850              // methods
851              public abstract f /* ()V */;
852            }
853        "#]]
854        .assert_eq(printed);
855    }
856
857    #[test]
858    fn pretty_prints_inner_classes_and_nesthost_details() {
859        let mut constant_pool = ConstantPool::default();
860        let outer_class = constant_pool.add_class("p/Outer").unwrap();
861        let inner_class = constant_pool.add_class("p/Outer$Inner").unwrap();
862        let super_class = constant_pool.add_class("java/lang/Object").unwrap();
863        let inner_name = constant_pool.add_utf8("Inner").unwrap();
864        let inner_classes_name = constant_pool.add_utf8("InnerClasses").unwrap();
865        let nest_host_name = constant_pool.add_utf8("NestHost").unwrap();
866
867        let class_file = ClassFile {
868            version: JAVA_21,
869            access_flags: ClassAccessFlags::PUBLIC,
870            constant_pool,
871            this_class: inner_class,
872            super_class,
873            interfaces: vec![],
874            fields: vec![],
875            methods: vec![],
876            attributes: vec![
877                Attribute::InnerClasses {
878                    name_index: inner_classes_name,
879                    classes: vec![InnerClass {
880                        class_info_index: inner_class,
881                        outer_class_info_index: outer_class,
882                        name_index: inner_name,
883                        access_flags: NestedClassAccessFlags::PRIVATE,
884                    }],
885                },
886                Attribute::NestHost {
887                    name_index: nest_host_name,
888                    host_class_index: outer_class,
889                },
890            ],
891        };
892
893        let printed = pretty_print_classfile(&class_file);
894        let printed = printed.as_str();
895
896        expect![[r#"
897            // version: 65.0 (Java 21)
898            public class p.Outer$Inner {
899
900              // methods
901
902              // class attributes
903              // InnerClasses:
904              // - inner: p.Outer$Inner outer: p.Outer name: Inner flags: (0x0002) ACC_PRIVATE
905              // NestHost: p.Outer
906            }
907        "#]]
908        .assert_eq(printed);
909    }
910
911    #[test]
912    fn pretty_prints_ldc_numeric_constants_by_value() {
913        let mut constant_pool = ConstantPool::default();
914        let int_index = constant_pool.add_integer(2147483647).unwrap();
915        let long_index = constant_pool.add_long(9223372036854775807).unwrap();
916
917        assert_eq!(
918            format_instruction(
919                &ristretto_classfile::attributes::Instruction::Ldc(int_index as u8),
920                &constant_pool
921            ),
922            "ldc 2147483647"
923        );
924        assert_eq!(
925            format_instruction(
926                &ristretto_classfile::attributes::Instruction::Ldc2_w(long_index),
927                &constant_pool
928            ),
929            "ldc 9223372036854775807"
930        );
931    }
932}