Skip to content
Merged
127 changes: 123 additions & 4 deletions src/compiler/code-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
ConstructNotSupportedError,
NoMethodMatchingSignatureError
} from './error'
import { unannTypeToString } from '../types/ast/utils'
import { FieldInfo, MethodInfos, SymbolInfo, SymbolTable, VariableInfo } from './symbol-table'

type Label = {
Expand Down Expand Up @@ -576,6 +577,123 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
return { stackSize: maxStack, resultType: resType }
},

TryStatement: (node: Node, cg: CodeGenerator) => {
let maxStack = 0
const { block, catches } = node as any

// If no catches, just compile the try block
if (!catches || !catches.catchClauses || catches.catchClauses.length === 0) {
return { stackSize: compile(block, cg).stackSize, resultType: EMPTY_TYPE }
}

// mark start of protected region
const tryStart = cg.generateNewLabel()
tryStart.offset = cg.code.length

// compile try block
maxStack = Math.max(maxStack, compile(block, cg).stackSize)

// end of protected region (first instruction after try block)
const tryEnd = cg.generateNewLabel()
tryEnd.offset = cg.code.length

// jump over handlers when try completes normally
const afterHandlers = cg.generateNewLabel()
cg.addBranchInstr(OPCODE.GOTO, afterHandlers)

// For each catch clause, emit a handler and an exception table entry
for (const catchClause of catches.catchClauses) {
const handlerLabel = cg.generateNewLabel()
handlerLabel.offset = cg.code.length

// determine catch type index (constant pool)
const catchTypeNode = catchClause.catchFormalParameter.catchType
const catchTypeName = unannTypeToString(catchTypeNode.unannClassType)
let catchClassName = 'java/lang/Throwable'
try {
catchClassName = cg.symbolTable.queryClass(catchTypeName).name
} catch (e) {
catchClassName = catchTypeName.includes('/') ? catchTypeName : catchTypeName.replace(/\./g, '/')
}
const catchTypeIndex = cg.constantPoolManager.indexClassInfo(catchClassName)

// add exception table entry (startPc, endPc, handlerPc, catchType)
cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: handlerLabel.offset, catchType: catchTypeIndex })

// create scope for catch variable
cg.symbolTable.extend()
const varName = catchClause.catchFormalParameter.variableDeclaratorId
const varTypeStr = unannTypeToString(catchTypeNode.unannClassType)
const varInfo = {
name: varName,
accessFlags: 0,
index: cg.maxLocals,
typeName: varTypeStr,
typeDescriptor: cg.symbolTable.generateFieldDescriptor(varTypeStr)
}
cg.symbolTable.insertVariableInfo(varInfo)
if (['J', 'D'].includes(varInfo.typeDescriptor)) {
cg.maxLocals += 2
} else {
cg.maxLocals++
}

// at handler entry, the exception object is on the stack; store it into the local
cg.code.push(OPCODE.ASTORE, varInfo.index)

// compile catch block statements
const catchBlock = catchClause.block
catchBlock.blockStatements.forEach((stmt: any) => {
const { stackSize } = compile(stmt, cg)
maxStack = Math.max(maxStack, stackSize)
})

// teardown catch scope
cg.symbolTable.teardown()

// after handler, jump to afterHandlers
cg.addBranchInstr(OPCODE.GOTO, afterHandlers)
}

// If finally exists, add a catch-all handler that runs finally then rethrows
const finallyNode: any = (node as any).finally
if (finallyNode) {
const catchAllLabel = cg.generateNewLabel()
catchAllLabel.offset = cg.code.length
cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: catchAllLabel.offset, catchType: 0 })

// allocate temp local to store exception
const tempIndex = cg.maxLocals
cg.maxLocals += 1
cg.code.push(OPCODE.ASTORE, tempIndex)

// compile finally block inside catch-all
finallyNode.blockStatements.forEach((stmt: any) => {
const { stackSize } = compile(stmt, cg)
maxStack = Math.max(maxStack, stackSize)
})
Comment thread
kjw142857 marked this conversation as resolved.
Outdated

// reload exception and rethrow
cg.code.push(OPCODE.ALOAD, tempIndex, OPCODE.ATHROW)

// normal finally path: compile finally once for normal/handled flows
const finallyLabel = cg.generateNewLabel()
finallyLabel.offset = cg.code.length
finallyNode.blockStatements.forEach((stmt: any) => {
const { stackSize } = compile(stmt, cg)
maxStack = Math.max(maxStack, stackSize)
})

// place after-handlers label
afterHandlers.offset = cg.code.length
} else {
// no finally: place after-handlers label
afterHandlers.offset = cg.code.length
}

return { stackSize: maxStack, resultType: EMPTY_TYPE }
},
Comment thread
kjw142857 marked this conversation as resolved.

TernaryExpression: (node: Node, cg: CodeGenerator) => {
let maxStack = 0
const {
Expand Down Expand Up @@ -1684,6 +1802,7 @@ class CodeGenerator {
constantPoolManager: ConstantPoolManager
maxLocals: number = 0
stackSize: number = 0
exceptionTable: Array<ExceptionHandler> = []
labels: Label[] = []
loopLabels: Label[][] = []
switchLabels: Label[] = []
Expand Down Expand Up @@ -1722,6 +1841,7 @@ class CodeGenerator {
generateCode(currentClass: string, methodNode: MethodDeclaration) {
this.symbolTable.extend()
this.currentClass = currentClass
this.exceptionTable = []
if (!methodNode.methodModifier.includes('static')) {
this.maxLocals++
}
Expand Down Expand Up @@ -1760,7 +1880,6 @@ class CodeGenerator {
}
this.resolveLabels()

const exceptionTable: Array<ExceptionHandler> = []
const attributes: Array<AttributeInfo> = []
const codeBuf = new Uint8Array(this.code).buffer
const dataView = new DataView(codeBuf)
Expand All @@ -1769,7 +1888,7 @@ class CodeGenerator {
const attributeLength =
12 +
this.code.length +
8 * exceptionTable.length +
8 * this.exceptionTable.length +
attributes.map(attr => attr.attributeLength + 6).reduce((acc, val) => acc + val, 0)
this.symbolTable.teardown()

Expand All @@ -1780,8 +1899,8 @@ class CodeGenerator {
maxLocals: this.maxLocals,
codeLength: this.code.length,
code: dataView,
exceptionTableLength: exceptionTable.length,
exceptionTable: exceptionTable,
exceptionTableLength: this.exceptionTable.length,
exceptionTable: this.exceptionTable,
attributesCount: attributes.length,
attributes: attributes
}
Expand Down
40 changes: 40 additions & 0 deletions src/jvm/__tests__/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ReferenceClassData } from '../types/class/ClassData'
import { JvmObject } from '../types/reference/Object'
import Thread from '../../jvm/thread'
import JVM from '../../jvm/jvm'
import { JavaStackFrame } from '../../jvm/stackframe'
import { setupTest, TestThreadPool } from './__utils__/test-utils'
import { METHOD_FLAGS } from '../../ClassFile/types/methods'

let thread: Thread
let threadClass: ReferenceClassData
Expand Down Expand Up @@ -67,4 +69,42 @@ describe('Thread', () => {
test('should manage wide (64-bit) values on the operand stack correctly', () => {
// TODO
})

test('should route an exception to a matching try-catch handler in the current method', () => {
const setup = setupTest()
const { testLoader, thread: testThread, classes } = setup
const exceptionMethodClass = testLoader.createClass({
className: 'TryCatchTest',
loader: testLoader,
methods: [
{
accessFlags: [METHOD_FLAGS.ACC_PUBLIC],
name: 'test0',
descriptor: '()V',
attributes: [],
code: new DataView(new ArrayBuffer(1)),
exceptionTable: [
{
startPc: 0,
endPc: 1,
handlerPc: 0,
catchType: 'java/lang/NullPointerException'
}
]
}
],
}) as ReferenceClassData

const method = exceptionMethodClass.getMethod('test0()V')
expect(method).not.toBeNull()

testThread.invokeStackFrame(
new JavaStackFrame(exceptionMethodClass, method as any, 0, [])
)
const exceptionObj = classes.NullPointerException.instantiate()
testThread.throwException(exceptionObj)

expect(testThread.getPC()).toBe(0)
expect(testThread.peekStackFrame().operandStack).toEqual([exceptionObj])
})
})
60 changes: 36 additions & 24 deletions src/jvm/exception-table.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
import { ClassData } from "./types/class/ClassData"

class Entry {
from: number
to: number
target: number
type: ClassData

constructor(from: number, to: number, target: number, type: ClassData) {
this.from = from;
this.to = to;
this.target = target;
this.type = type;
}
import { ClassData } from './types/class/ClassData'

export interface ExceptionTableEntry {
startPc: number
endPc: number
handlerPc: number
catchType: any | null
}

export class ExceptionTable {
private entries: Entry[]
export class ExceptionTable implements Iterable<ExceptionTableEntry> {
private entries: ExceptionTableEntry[]

constructor(entries?: ExceptionTableEntry[]) {
this.entries = entries ? entries.slice() : []
}

retrieve(line: number): Entry | null {
this.entries.forEach(entry => {
if (line >= entry.from && line <= entry.to) {
return entry
retrieve(pc: number): ExceptionTableEntry | null {
for (let i = 0; i < this.entries.length; i++) {
const e = this.entries[i]
if (pc >= e.startPc && pc < e.endPc) {
return e
}
})
}
return null
}

insert(from: number, to: number, target: number, type: ClassData): void {
var entry = new Entry(from, to, target, type)
this.entries.push(entry)
insert(startPc: number, endPc: number, handlerPc: number, catchType: ClassData | null): void {
this.entries.push({ startPc, endPc, handlerPc, catchType })
}

toArray(): ExceptionTableEntry[] {
return this.entries.slice()
}

[Symbol.iterator](): Iterator<ExceptionTableEntry> {
return this.entries[Symbol.iterator]()
}
forEach(cb: (entry: ExceptionTableEntry, idx?: number) => void) {
this.entries.forEach(cb)
}

get length() {
return this.entries.length
}
}
12 changes: 5 additions & 7 deletions src/jvm/types/class/Attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SourceFileAttribute,
StackMapFrame
} from '../../../ClassFile/types/attributes'
import { ExceptionTable } from '../../exception-table'
import { ConstantPool } from '../../constant-pool'
import {
ConstantClass,
Expand Down Expand Up @@ -45,7 +46,8 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool):
case 'Code':
const code = info as CodeAttribute
const attr: { [attributeName: string]: IAttribute } = {}
const exceptionTable = code.exceptionTable.map(handler => {
const exceptionTable = new ExceptionTable(
code.exceptionTable.map(handler => {
return {
startPc: handler.startPc,
endPc: handler.endPc,
Expand All @@ -54,6 +56,7 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool):
handler.catchType === 0 ? null : (constantPool.get(handler.catchType) as ConstantClass)
}
})
)
code.attributes.forEach(element => {
attr[(constantPool.get(element.attributeNameIndex) as ConstantUtf8).get()] = info2Attribute(
element,
Expand Down Expand Up @@ -244,12 +247,7 @@ export interface Code extends IAttribute {
codeLength: number
code: DataView
exceptionTableLength: number
exceptionTable: Array<{
startPc: number
endPc: number
handlerPc: number
catchType: ConstantClass | null
}>
exceptionTable: ExceptionTable
attributes: {
[attributeName: string]: IAttribute
}
Expand Down
3 changes: 2 additions & 1 deletion src/jvm/types/class/Method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { attrInfo2Interface, parseMethodDescriptor, getArgs, logger } from '../.
import { ErrorResult, ImmediateResult, ResultType, SuccessResult } from '../Result'
import { JavaType, JvmObject } from '../reference/Object'
import { Code, Exceptions, IAttribute, NestHost, Signature } from './Attributes'
import { ExceptionTable } from '../../exception-table'
import { ReferenceClassData, ArrayClassData, ClassData } from './ClassData'
import { ConstantClass, ConstantMethodref, ConstantNameAndType, ConstantUtf8 } from './Constants'

Expand Down Expand Up @@ -484,7 +485,7 @@ export class Method {
codeLength: dv.buffer.byteLength,
code: dv,
exceptionTableLength: 0,
exceptionTable: [],
exceptionTable: new ExceptionTable(),
attributes: {}
} as Code
},
Expand Down
2 changes: 1 addition & 1 deletion src/jvm/utils/disassembler/utils/readAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ function readCodeAttribute(
throw new Error('Class format error: Code attribute invalid length')
}

const code = new DataView(view.buffer, offset, codeLength)
const code = new DataView(view.buffer, view.byteOffset + offset, codeLength)
offset += codeLength

const exceptionTableLength = view.getUint16(offset)
Expand Down
Loading