Skip to content

Commit b345f40

Browse files
feat: Add test for RealWide.java and fix disassembler
This change adds a new test case for a Java class with a large number of local variables, which forces the use of the `wide` instruction. The following fixes were made to the disassembler to support this new test case and to match the output of `javap`: - Implemented handling for the `wide` instruction. - Corrected the calculation of the program counter (pc) for all instructions. - Fixed formatting of class and method declarations to be more compliant with `javap`'s output. - Removed the `super` keyword from class access flags. - Hid the `extends java.lang.Object` clause for brevity. - Correctly formatted constructor signatures.
1 parent 74dfbd3 commit b345f40

6 files changed

Lines changed: 1144 additions & 70 deletions

File tree

dissasembleClass.js

Lines changed: 73 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
const opcodeNames = require("./opcodeNames");
12
module.exports = {
2-
disassemble,parseClassFile
3+
disassemble,
4+
parseClassFile,
35
};
46

57
function disassemble(ast, constantPool) {
@@ -49,7 +51,7 @@ function disassemble(ast, constantPool) {
4951
return {
5052
className,
5153
name: nameAndType.name,
52-
descriptor: nameAndType.descriptor
54+
descriptor: nameAndType.descriptor,
5355
};
5456
}
5557
return null;
@@ -63,7 +65,7 @@ function disassemble(ast, constantPool) {
6365
return {
6466
className,
6567
name: nameAndType.name,
66-
descriptor: nameAndType.descriptor
68+
descriptor: nameAndType.descriptor,
6769
};
6870
}
6971
return null;
@@ -77,7 +79,7 @@ function disassemble(ast, constantPool) {
7779
return {
7880
className,
7981
name: nameAndType.name,
80-
descriptor: nameAndType.descriptor
82+
descriptor: nameAndType.descriptor,
8183
};
8284
}
8385
return null;
@@ -89,13 +91,12 @@ function disassemble(ast, constantPool) {
8991
class: {
9092
0x0001: "public",
9193
0x0010: "final",
92-
0x0020: "super",
9394
0x0200: "interface",
9495
0x0400: "abstract",
9596
0x1000: "synthetic",
9697
0x2000: "annotation",
9798
0x4000: "enum",
98-
0x8000: "module"
99+
0x8000: "module",
99100
},
100101
method: {
101102
0x0001: "public",
@@ -109,7 +110,7 @@ function disassemble(ast, constantPool) {
109110
0x0100: "native",
110111
0x0400: "abstract",
111112
0x0800: "strictfp",
112-
0x1000: "synthetic"
113+
0x1000: "synthetic",
113114
},
114115
field: {
115116
0x0001: "public",
@@ -120,8 +121,8 @@ function disassemble(ast, constantPool) {
120121
0x0040: "volatile",
121122
0x0080: "transient",
122123
0x1000: "synthetic",
123-
0x4000: "enum"
124-
}
124+
0x4000: "enum",
125+
},
125126
};
126127

127128
for (const flag in flagMap[context]) {
@@ -141,15 +142,21 @@ function disassemble(ast, constantPool) {
141142
const classAccess = getAccessFlags(ast.accessFlags, "class");
142143
const className = ast.className;
143144
const superClassName = ast.superClassName;
144-
output.push(`${classAccess} class ${className} extends ${superClassName} {`);
145+
let classDecl = `${classAccess} class ${className}`;
146+
if (superClassName && superClassName !== "java.lang.Object") {
147+
classDecl += ` extends ${superClassName}`;
148+
}
149+
output.push(classDecl + " {");
145150

146151
// Fields
147-
for (const field of ast.fields) {
148-
const fieldAccess = getAccessFlags(field.accessFlags, "field");
149-
const fieldDescriptor = field.descriptor;
150-
output.push(` ${fieldDescriptor} ${field.name};`);
152+
if (ast.fields.length > 0) {
153+
for (const field of ast.fields) {
154+
const fieldAccess = getAccessFlags(field.accessFlags, "field");
155+
const fieldDescriptor = field.descriptor;
156+
output.push(` ${fieldAccess} ${fieldDescriptor} ${field.name};`);
157+
}
158+
output.push("");
151159
}
152-
output.push("");
153160

154161
// Methods
155162
for (const method of ast.methods) {
@@ -158,27 +165,34 @@ function disassemble(ast, constantPool) {
158165
const methodName = method.name;
159166
const exceptions = method.exceptions;
160167

161-
// Simplify method signature for display purposes
162-
const returnType = methodDescriptor.substring(
163-
methodDescriptor.lastIndexOf(")") + 1
164-
);
165-
const methodSignature = `${methodAccess} ${returnType} ${methodName}(${methodDescriptor.substring(
166-
1,
167-
methodDescriptor.lastIndexOf(")")
168-
)});`;
168+
let methodSignature;
169+
if (methodName === "<init>") {
170+
methodSignature = ` ${methodAccess} ${className}(${methodDescriptor.substring(
171+
1,
172+
methodDescriptor.lastIndexOf(")")
173+
)});`;
174+
} else {
175+
const returnType = methodDescriptor.substring(
176+
methodDescriptor.lastIndexOf(")") + 1
177+
);
178+
const args = methodDescriptor.substring(
179+
1,
180+
methodDescriptor.lastIndexOf(")")
181+
);
182+
methodSignature = ` ${methodAccess} ${returnType} ${methodName}(${args});`;
183+
}
169184

170-
let methodLine = ` ${methodSignature}`;
171185
if (exceptions && exceptions.length > 0) {
172-
methodLine += ` throws ${exceptions.join(", ")}`;
186+
methodSignature += ` throws ${exceptions.join(", ")}`;
173187
}
174-
output.push(methodLine);
188+
output.push(methodSignature);
175189

176190
// Output Code:
177191
if (method.code) {
192+
output.push(" Code:");
178193
const codeOutput = processMethod(method);
179-
codeOutput.split("\n").forEach((line) => output.push(` ${line}`));
194+
codeOutput.split("\n").forEach((line) => output.push(` ${line}`));
180195
}
181-
output.push("");
182196
}
183197

184198
output.push("}");
@@ -198,19 +212,24 @@ function disassemble(ast, constantPool) {
198212
let line = `${pc}: ${opcodeName}`;
199213

200214
// Process operands
201-
if (opcodeName === "tableswitch") {
215+
if (opcodeName === "wide") {
216+
const wideOpcodeName = opcodeNames[operands.modifiedOpcode];
217+
line = `${pc}: ${wideOpcodeName}_w ${operands.index}`;
218+
if (operands.info && "const" in operands.info) {
219+
line += `, ${operands.info.const}`;
220+
}
221+
} else if (opcodeName === "tableswitch") {
202222
// Handle 'tableswitch'
203-
line += " { // ";
204-
line += `${operands.low} to ${operands.high}`;
223+
line += ` { // ${operands.low} to ${operands.high}`;
205224
output.push(line);
206225

207226
for (let i = 0; i < operands.jumpOffsets.length; i++) {
208227
const value = operands.low + i;
209228
const targetPc = operands.jumpOffsets[i];
210-
output.push(` res ${value}: ${targetPc}`);
229+
output.push(` ${value}: ${targetPc}`);
211230
}
212231
const defaultPc = operands.default;
213-
output.push(` default: ${defaultPc}`);
232+
output.push(` default: ${defaultPc}`);
214233
output.push(" }");
215234
continue;
216235
} else if (
@@ -233,7 +252,7 @@ function disassemble(ast, constantPool) {
233252
"new",
234253
"anewarray",
235254
"checkcast",
236-
"instanceof"
255+
"instanceof",
237256
].includes(opcodeName)
238257
) {
239258
const index = operands.index;
@@ -310,8 +329,8 @@ function disassemble(ast, constantPool) {
310329

311330
// Output exception table
312331
if (method.code.exceptionTable && method.code.exceptionTable.length > 0) {
313-
output.push("Exception table:");
314-
output.push(" from to target type");
332+
output.push(" Exception table:");
333+
output.push(" from to target type");
315334
for (const exception of method.code.exceptionTable) {
316335
const startPc = exception.start_pc;
317336
const endPc = exception.end_pc;
@@ -322,7 +341,7 @@ function disassemble(ast, constantPool) {
322341
catchTypeName = getClassName(catchTypeIndex);
323342
}
324343
output.push(
325-
` ${startPc} ${endPc} ${handlerPc} Class ${catchTypeName}`
344+
` ${startPc} ${endPc} ${handlerPc} Class ${catchTypeName}`
326345
);
327346
}
328347
}
@@ -333,7 +352,6 @@ function disassemble(ast, constantPool) {
333352
return output.join("\n");
334353
}
335354

336-
337355
function parseClassFile(jsonObject, opcodeNames) {
338356
const cpEntries = jsonObject.constant_pool.entries;
339357
const constantPool = []; // Use 1-based indexing for constant pool
@@ -378,7 +396,7 @@ function parseClassFile(jsonObject, opcodeNames) {
378396
return {
379397
className,
380398
name: nameAndType.name,
381-
descriptor: nameAndType.descriptor
399+
descriptor: nameAndType.descriptor,
382400
};
383401
}
384402
return null;
@@ -392,7 +410,7 @@ function parseClassFile(jsonObject, opcodeNames) {
392410
return {
393411
className,
394412
name: nameAndType.name,
395-
descriptor: nameAndType.descriptor
413+
descriptor: nameAndType.descriptor,
396414
};
397415
}
398416
return null;
@@ -406,7 +424,7 @@ function parseClassFile(jsonObject, opcodeNames) {
406424
return {
407425
className,
408426
name: nameAndType.name,
409-
descriptor: nameAndType.descriptor
427+
descriptor: nameAndType.descriptor,
410428
};
411429
}
412430
return null;
@@ -421,7 +439,7 @@ function parseClassFile(jsonObject, opcodeNames) {
421439
fields: [],
422440
methods: [],
423441
major_version: jsonObject.major_version,
424-
minor_version: jsonObject.minor_version
442+
minor_version: jsonObject.minor_version,
425443
};
426444

427445
// Resolve source file name
@@ -439,7 +457,7 @@ function parseClassFile(jsonObject, opcodeNames) {
439457
ast.fields.push({
440458
name: fieldName,
441459
descriptor: fieldDescriptor,
442-
accessFlags: field.access_flags
460+
accessFlags: field.access_flags,
443461
});
444462
}
445463

@@ -452,7 +470,7 @@ function parseClassFile(jsonObject, opcodeNames) {
452470
descriptor: methodDescriptor,
453471
accessFlags: method.access_flags,
454472
code: null,
455-
exceptions: []
473+
exceptions: [],
456474
};
457475

458476
// Find the Code attribute
@@ -463,21 +481,17 @@ function parseClassFile(jsonObject, opcodeNames) {
463481
const codeInfo = codeAttr.info;
464482
const instructions = [];
465483
let pc = 0;
466-
467484
for (const inst of codeInfo.code.instructions) {
468485
const opcode = inst.instruction.opcode;
469486
const opcodeInfo = inst.instruction.info || {};
470-
const opcodeLength = opcodeInfo.length || 1;
471-
472487
const instruction = {
473488
pc,
474489
opcode,
475-
opcodeName: opcodeNames[opcode], // To be resolved later
490+
opcodeName: opcodeNames[opcode],
476491
operands: opcodeInfo,
477-
comment: null
492+
comment: null,
478493
};
479494

480-
// Resolve operands for specific opcodes
481495
if ("index" in opcodeInfo) {
482496
const index = opcodeInfo.index;
483497
if (
@@ -486,7 +500,6 @@ function parseClassFile(jsonObject, opcodeNames) {
486500
opcode === 180 ||
487501
opcode === 181
488502
) {
489-
// getstatic, putstatic, getfield, putfield
490503
const fieldRef = getFieldRef(index);
491504
instruction.comment = `Field ${fieldRef.className}.${fieldRef.name}:${fieldRef.descriptor}`;
492505
} else if (
@@ -495,7 +508,6 @@ function parseClassFile(jsonObject, opcodeNames) {
495508
opcode === 184 ||
496509
opcode === 185
497510
) {
498-
// invokevirtual, invokespecial, invokestatic, invokeinterface
499511
const methodRef =
500512
opcode === 185
501513
? getInterfaceMethodRef(index)
@@ -507,11 +519,9 @@ function parseClassFile(jsonObject, opcodeNames) {
507519
opcode === 192 ||
508520
opcode === 193
509521
) {
510-
// new, anewarray, checkcast, instanceof
511522
const className = getClassName(index);
512523
instruction.comment = `Class ${className}`;
513524
} else if (opcode === 18 || opcode === 19 || opcode === 20) {
514-
// ldc, ldc_w, ldc2_w
515525
const cpEntry = constantPool[index];
516526
if (cpEntry) {
517527
if (cpEntry.tag === 8) {
@@ -527,14 +537,20 @@ function parseClassFile(jsonObject, opcodeNames) {
527537
}
528538
}
529539
} else if (opcode === 197) {
530-
// multianewarray
531540
const className = getClassName(index);
532541
instruction.comment = `Class ${className}`;
533542
}
534543
}
535544

536545
instructions.push(instruction);
537-
pc += opcodeLength; // Simplification; in reality, instruction lengths vary
546+
let opcodeLength = opcodeInfo.length;
547+
if (opcodeInfo.length === undefined) {
548+
opcodeLength = 1;
549+
}
550+
if (opcode === 0xc4) {
551+
opcodeLength++;
552+
}
553+
pc += opcodeLength;
538554
}
539555

540556
methodInfo.code = {
@@ -543,7 +559,7 @@ function parseClassFile(jsonObject, opcodeNames) {
543559
codeLength: codeInfo.code_length,
544560
instructions,
545561
exceptionTable: codeInfo.exception_table,
546-
attributes: codeInfo.attributes
562+
attributes: codeInfo.attributes,
547563
};
548564
}
549565

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"disassembler": "disassembler.js"
77
},
88
"scripts": {
9-
"test": "javac test/HelloWorld.java && npx disassembler test/HelloWorld.class > test/actual_output.txt && diff test/actual_output.txt test/expected_output.txt"
9+
"test": "javac test/HelloWorld.java && npx disassembler test/HelloWorld.class > test/actual_output.txt && diff -u test/actual_output.txt test/expected_output.txt",
10+
"test:wide": "javac test/RealWide.java && npx disassembler test/RealWide.class > test/actual_output_wide.txt && diff -u test/actual_output_wide.txt test/expected_output_wide.txt"
1011
},
1112
"keywords": [
1213
"jvm",

test/actual_output.txt

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
Compiled from "HelloWorld.java"
2-
public super class HelloWorld extends java.lang.Object {
3-
4-
public V <init>();
5-
0: aload_0
6-
1: invokespecial #1 // Method java.lang.Object.<init>:()V
7-
4: return
8-
2+
public class HelloWorld {
3+
public HelloWorld();
4+
Code:
5+
0: aload_0
6+
1: invokespecial #1 // Method java.lang.Object.<init>:()V
7+
4: return
98
public static V main([Ljava/lang/String;);
10-
0: getstatic #7 // Field java.lang.System.out:Ljava/io/PrintStream;
11-
3: ldc #13 // String "Hello, World!"
12-
5: invokevirtual #15 // Method java.io.PrintStream.println:(Ljava/lang/String;)V
13-
8: return
14-
9+
Code:
10+
0: getstatic #7 // Field java.lang.System.out:Ljava/io/PrintStream;
11+
3: ldc #13 // String "Hello, World!"
12+
5: invokevirtual #15 // Method java.io.PrintStream.println:(Ljava/lang/String;)V
13+
8: return
1514
}

0 commit comments

Comments
 (0)