diff --git a/input/multipleClasses.java b/input/multipleClasses.java index 927f542..0aca6f0 100644 --- a/input/multipleClasses.java +++ b/input/multipleClasses.java @@ -1,4 +1,4 @@ -package de.multipleclasses; +package out; class AClass { private int value; @@ -29,4 +29,4 @@ public AClass createNewInstance() { // return 1; // } -} \ No newline at end of file +} diff --git a/input/primitiveTypes.java b/input/primitiveTypes.java index dc93004..cc9849d 100644 --- a/input/primitiveTypes.java +++ b/input/primitiveTypes.java @@ -1,30 +1,27 @@ package de.primitivetypes; -public class A { - +public class primitiveTypes { public void testTypes() { // Integer type int a = 5; int b = 10; int c = a + b; int negative = -4; + // Boolean type boolean flag1 = true; boolean flag2 = false; boolean flag3 = flag1 && flag2; - /* - // Byte type byte byteVar1 = 127b; - byte byteVar2 = 0B; - byte byteVar3 = 12b + 24B; - + byte byteVar2 = 0b; + byte byteVar3 = 12b + 24b; // Short type short shortVar1 = 32767s; - short shortVar2 = 0S; - short shortVar3 = 15s % 4S; + short shortVar2 = 0s; + short shortVar3 = 15s % 4s; // Long type long longVar1 = 100000L; @@ -45,7 +42,6 @@ public void testTypes() { char charVar1 = 'A'; char charVar2 = 'B'; char charVar3 = 'C' + 'D'; - - */ + return; } } \ No newline at end of file diff --git a/src/main/scala/de/students/ByteCodeGenerator/ASMHelpers.scala b/src/main/scala/de/students/ByteCodeGenerator/ASMHelpers.scala index b3e8691..ba32105 100644 --- a/src/main/scala/de/students/ByteCodeGenerator/ASMHelpers.scala +++ b/src/main/scala/de/students/ByteCodeGenerator/ASMHelpers.scala @@ -28,22 +28,44 @@ private def visibilityModifier(methodDecl: MethodDecl): Int = { + ACC_PUBLIC } -private def javaifyClass(fullName: String) = fullName.replace('.', '/') +private def typeStackSize(t: Type): Int = t match { + case IntType => 1 + case BoolType => 1 + case ShortType => 1 + case LongType => 2 + case ByteType => 1 + case FloatType => 1 + case DoubleType => 2 + case CharType => 1 + case ArrayType(_) => 1 + case UserType(_) => 1 + case _ => throw ByteCodeGeneratorException(f"type $t can't be pushed onto the stack") +} + +private def javaifyClass(fullName: String) = makeObjectClassName(fullName.replace('.', '/')) +private def makeObjectClassName(name: String) = + if name.contains("Object") then "java/lang/Object" else name // TODO temporary, make issue for type check -private def asmType(t: Type): String = t match { - case NoneType => "" // TODO find out descriptor of NoneType +private def javaSignature(t: Type): String = t match { case IntType => "I" case BoolType => "Z" case VoidType => "V" - case ArrayType(baseType) => f"[${asmType(baseType)}" + case ShortType => "S" + case LongType => "L" + case ByteType => "B" + case FloatType => "F" + case DoubleType => "D" + case CharType => "C" + case ArrayType(baseType) => f"[${javaSignature(baseType)}" case UserType(name) => { f"L${javaifyClass(name)};" } case FunctionType(returnType, parameterTypes) => { - val parameters = parameterTypes.map(asmType).fold("")((a, b) => a + b) - f"($parameters)${asmType(returnType)}" + val parameters = parameterTypes.map(javaSignature).fold("")((a, b) => a + b) + f"($parameters)${javaSignature(returnType)}" } - case _ => throw ByteCodeGeneratorException(s"Unknown type $t cannot be converted to ASM-type") + case NoneType => "" // NOTE should not happen + case _ => throw ByteCodeGeneratorException(s"Unknown type $t cannot be converted to ASM-type") } private def asmUserType(t: Type): String = t match { @@ -52,7 +74,7 @@ private def asmUserType(t: Type): String = t match { } private def asmConstructorType(parameters: List[Type]): String = { - asmType(FunctionType(VoidType, parameters)) + javaSignature(FunctionType(VoidType, parameters)) } private def functionType(methodDecl: MethodDecl): FunctionType = @@ -61,69 +83,22 @@ private def functionType(methodDecl: MethodDecl): FunctionType = private def constructorType(constructorDecl: ConstructorDecl): FunctionType = FunctionType(VoidType, constructorDecl.params.map(param => param.varType)) -private def binaryOpcode(op: String, t: Type): String = { - val prefix = t match { - case IntType => "I" - case BoolType => "I" // NOTE: this should be Z - case _ => throw ByteCodeGeneratorException(f"the type $t is not allowed") - } - val opName = op match { - case "+" => "ADD" - case "-" => "SUB" - case "*" => "MUL" - case "/" => "DIV" - case "%" => "REM" - case _ => throw ByteCodeGeneratorException(f"the operator \"$op\" is not allowed for binary operations") - } - prefix + opName -} -private def asmOpcode(opName: String): Int = opName match { - case "IADD" => IADD - case "ISUB" => ISUB - case "IMUL" => IMUL - case "IDIV" => IDIV - case "IREM" => IREM - case _ => throw NotImplementedError("asm opcode") -} - -private def isBooleanOpcode(op: String): Boolean = +private def isBooleanOpcode(op: String): Boolean = { op == "==" || - op == "!=" || - op == "<" || - op == "<=" || - op == ">" || - op == ">=" || - op == "&&" || - op == "||" - -private def asmLoadInsn(t: Type): Int = t match { - case IntType => ILOAD - case BoolType => ILOAD - case ArrayType(baseType) => ALOAD - case UserType(name) => ALOAD - case _ => throw ByteCodeGeneratorException(f"type ${asmType(t)} can not be fetched as variable") -} -private def asmStoreInsn(t: Type): Int = t match { - case IntType => ISTORE - case BoolType => ISTORE - case ArrayType(baseType) => ASTORE - case UserType(name) => ASTORE - case _ => throw ByteCodeGeneratorException(f"type ${asmType(t)} can not be saved as variable") -} - -private def asmReturnCode(t: Type): Int = t match { - case IntType => IRETURN - case BoolType => IRETURN - case ArrayType(baseType) => ARETURN - case UserType(name) => ARETURN - case _ => throw ByteCodeGeneratorException(f"return type \"$t\" is not allowed") + op == "!=" || + op == "<" || + op == "<=" || + op == ">" || + op == ">=" || + op == "&&" || + op == "||" } private def makePrintStatement(toPrint: Expression, methodVisitor: MethodVisitor, state: MethodGeneratorState): Unit = { methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;") val t = generateTypedExpression(toPrint.asInstanceOf[TypedExpression], state) state.stackDepth += 1 - methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", f"(${asmType(t)})V", false) + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", f"(${javaSignature(t)})V", false) } private def debugLogStack(state: MethodGeneratorState, where: String): Unit = { diff --git a/src/main/scala/de/students/ByteCodeGenerator/ByteCodeGenerator.scala b/src/main/scala/de/students/ByteCodeGenerator/ByteCodeGenerator.scala index b8628af..ba2172d 100644 --- a/src/main/scala/de/students/ByteCodeGenerator/ByteCodeGenerator.scala +++ b/src/main/scala/de/students/ByteCodeGenerator/ByteCodeGenerator.scala @@ -27,7 +27,7 @@ private def generateClassBytecode(classDecl: ClassDecl): ClassBytecode = { val classWriter = new ClassWriter(0) val javaClassName = javaifyClass(classDecl.name) - val parent = "java/lang/Object" // TODO use real parent + val parent = javaifyClass(classDecl.parent) // "java/lang/Object" // TODO use real parent // set class header classWriter.visit( @@ -61,7 +61,7 @@ private def generateClassBytecode(classDecl: ClassDecl): ClassBytecode = { .visitField( accessModifier(fieldDecl), fieldDecl.name, - asmType(fieldDecl.varType), + javaSignature(fieldDecl.varType), null, // signature null // initial value, only used for static fields ) @@ -88,7 +88,7 @@ private def generateConstructor( val methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "", - asmType(constructorType(constructorDecl)), + javaSignature(constructorType(constructorDecl)), null, null ) @@ -100,7 +100,13 @@ private def generateConstructor( methodVisitor.visitCode() methodVisitor.visitVarInsn(ALOAD, 0) // load this - methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false) // call Object constructor + methodVisitor.visitMethodInsn( + INVOKESPECIAL, + javaifyClass(classDecl.parent), + "", + "()V", + false + ) // call Object constructor generateStatement(constructorDecl.body, state) @@ -114,7 +120,7 @@ private def generateMethodBody(classDecl: ClassDecl, methodDecl: MethodDecl, cla val methodVisitor = classWriter.visitMethod( visibilityModifier(methodDecl), methodDecl.name, - asmType(functionType(methodDecl)), + javaSignature(functionType(methodDecl)), null, // signature null // exceptions ) @@ -129,12 +135,12 @@ private def generateMethodBody(classDecl: ClassDecl, methodDecl: MethodDecl, cla methodVisitor.visitCode() - state.stackDepth = 0 + state.stackDepth = state.localVariableCount if (methodDecl.body.isDefined) { generateStatement(methodDecl.body.get, state) } - methodVisitor.visitMaxs(state.maxStackDepth + 1, state.localVariableCount) + methodVisitor.visitMaxs(state.maxStackDepth, state.localVariableCount) methodVisitor.visitEnd() } @@ -149,7 +155,8 @@ private case class MethodGeneratorState( val scopeEnds: ArrayBuffer[Label], val loopStarts: ArrayBuffer[Label], var currentScope: Int, - val variables: mutable.HashMap[String, VariableInfo] + val variables: mutable.HashMap[String, VariableInfo], + val stackTypes: ArrayBuffer[Type] ) { def startSimpleScope(end: Label): Unit = { scopeEnds += end @@ -171,12 +178,27 @@ private case class MethodGeneratorState( endSimpleScope() } - def pushStack(): Unit = { - stackDepth += 1 + def pushStack(t: Type): Unit = { + stackTypes.append(t) + pushStack(typeStackSize(t)) + } + private def pushStack(count: Int): Unit = { + stackDepth += count maxStackDepth = Math.max(stackDepth, maxStackDepth) } + def popStack(): Unit = { + if (stackTypes.isEmpty) { + throw ByteCodeGeneratorException("empty stack cannot be popped, this is a bug in the bytecode generator") + } + val typeSize = typeStackSize(stackTypes.last) + + stackDepth -= typeSize + stackTypes.remove(stackTypes.size - 1) + } def popStack(count: Int): Unit = { - stackDepth -= count + for (i <- 0 until count) { + popStack() + } } def addVariable(name: String, t: Type): Int = { @@ -189,8 +211,9 @@ private case class MethodGeneratorState( if (res.isDefined) { throw ByteCodeGeneratorException(f"variable $name already exists") } - localVariableCount += 1 - localVariableCount - 1 + val typeSize = typeStackSize(t) + localVariableCount += typeSize + localVariableCount - typeSize } def getVariable(name: String): VariableInfo = { @@ -226,7 +249,8 @@ private def defaultMethodGeneratorState( ArrayBuffer.empty, ArrayBuffer.empty, 0, - mutable.HashMap.empty + mutable.HashMap.empty, + ArrayBuffer(UserType("this")) // TODO use own type ) private case class VariableInfo( diff --git a/src/main/scala/de/students/ByteCodeGenerator/Expressions.scala b/src/main/scala/de/students/ByteCodeGenerator/Expressions.scala index aba8449..c960223 100644 --- a/src/main/scala/de/students/ByteCodeGenerator/Expressions.scala +++ b/src/main/scala/de/students/ByteCodeGenerator/Expressions.scala @@ -11,8 +11,8 @@ private def generateExpression(expression: Expression, state: MethodGeneratorSta expression match { case TypedExpression(variableReference: VarRef, _) => generateVariableReference(variableReference, state) - case TypedExpression(literal: Literal, _) => - generateLiteral(literal, state) + case TypedExpression(literal: Literal, t) => + generateLiteral(literal, t, state) case TypedExpression(binaryOperation: BinaryOp, _) => generateBinaryOperation(binaryOperation, state) case TypedExpression(methodCall: MethodCall, returnType) => @@ -47,8 +47,8 @@ private def generateVariableReference(varRef: VarRef, state: MethodGeneratorStat } // LITERAL -private def generateLiteral(literal: Literal, state: MethodGeneratorState): Unit = { - Instructions.pushConstant(literal.value, state) +private def generateLiteral(literal: Literal, literalType: Type, state: MethodGeneratorState): Unit = { + Instructions.pushConstant(literal.value, literalType, state) debugLogStack(state, f"pushed $literal") } @@ -65,7 +65,7 @@ private def generateBinaryOperation(operation: BinaryOp, state: MethodGeneratorS val expressionType = generateTypedExpression(operation.left.asInstanceOf[TypedExpression], state) generateExpression(operation.right, state) // should be same type, this is not our problem if not - val opcode = asmOpcode(binaryOpcode(operation.op, expressionType)) + val opcode = binaryOpcode(operation.op, expressionType) Instructions.binaryOperation(opcode, state) } @@ -93,7 +93,7 @@ private def generateBooleanOperation(operation: BinaryOp, state: MethodGenerator Instructions.visitLabel(end, state) // the stack counter has to be manually decreased to account for branching - state.popStack(1) + state.popStack() } else if (operation.op == "||") { val truePush = Label() val end = Label() @@ -109,7 +109,7 @@ private def generateBooleanOperation(operation: BinaryOp, state: MethodGenerator Instructions.visitLabel(end, state) // the stack counter has to be manually decreased to account for branching - state.popStack(1) + state.popStack() } else { val ifInsn = operation.op match { case "==" => IFEQ @@ -166,7 +166,7 @@ private def generateVariableAccess(varName: String, rvalue: Expression, state: M } else { generateExpression(rvalue, state) - Instructions.duplicateTop(state) + Instructions.duplicateTopType(state) Instructions.storeVar(variableInfo.id, variableInfo.t, state) } @@ -182,7 +182,7 @@ private def generateTypedExpression(expression: TypedExpression, state: MethodGe private def generateMethodCall(methodCall: MethodCall, returnType: Type, state: MethodGeneratorState): Unit = { val classType = generateTypedExpression(methodCall.target.asInstanceOf[TypedExpression], state) val parameterTypes = methodCall.args.map(expr => generateTypedExpression(expr.asInstanceOf[TypedExpression], state)) - val methodDescriptor = asmType(FunctionType(returnType, parameterTypes)) + val methodDescriptor = javaSignature(FunctionType(returnType, parameterTypes)) Instructions.callMethod(asmUserType(classType), methodCall.methodName, methodCall.args.size, methodDescriptor, state) } diff --git a/src/main/scala/de/students/ByteCodeGenerator/Instructions.scala b/src/main/scala/de/students/ByteCodeGenerator/Instructions.scala index 903c20f..4393323 100644 --- a/src/main/scala/de/students/ByteCodeGenerator/Instructions.scala +++ b/src/main/scala/de/students/ByteCodeGenerator/Instructions.scala @@ -6,17 +6,17 @@ import org.objectweb.asm.Opcodes.* import de.students.Parser.* private object Instructions { - def pushConstant(constant: Any, state: MethodGeneratorState): Unit = { - state.pushStack() + def pushConstant(constant: Any, constantType: Type, state: MethodGeneratorState): Unit = { + state.pushStack(constantType) state.methodVisitor.visitLdcInsn(constant) } def pushTrue(state: MethodGeneratorState): Unit = { - pushConstant(1, state) + pushConstant(1, BoolType, state) } def pushFalse(state: MethodGeneratorState): Unit = { - pushConstant(0, state) + pushConstant(0, BoolType, state) } def goto(label: Label, state: MethodGeneratorState): Unit = { @@ -50,7 +50,7 @@ private object Instructions { */ def condJump(opcode: Int, label: Label, state: MethodGeneratorState): Unit = { state.methodVisitor.visitJumpInsn(opcode, label) - state.popStack(1) + state.popStack() } def switch(default: Label, labels: Array[Label], keys: Array[Int], state: MethodGeneratorState) = { @@ -60,43 +60,57 @@ private object Instructions { def storeVar(varId: Int, varType: Type, state: MethodGeneratorState) = { // TODO multiple types state.methodVisitor.visitVarInsn(asmStoreInsn(varType), varId) - state.popStack(1) + state.popStack() } def loadVar(varId: Int, varType: Type, state: MethodGeneratorState) = { // TODO multiple types state.methodVisitor.visitVarInsn(asmLoadInsn(varType), varId) - state.pushStack() + state.pushStack(varType) } + def popType(state: MethodGeneratorState) = { + val size = typeStackSize(state.stackTypes.last) + if (size == 2) { popTwo(state) } + else { for (i <- 0 until size) { pop(state) } } + } def pop(state: MethodGeneratorState) = { state.methodVisitor.visitInsn(POP) - state.popStack(1) + state.popStack() + } + def popTwo(state: MethodGeneratorState) = { + state.methodVisitor.visitInsn(POP2) + state.popStack() } + def duplicateTopType(state: MethodGeneratorState) = { + val size = typeStackSize(state.stackTypes.last) + if (size == 2) { duplicateTopTwo(state) } + else { for (i <- 0 until size) { duplicateTop(state) } } + } def duplicateTop(state: MethodGeneratorState) = { state.methodVisitor.visitInsn(DUP) - state.pushStack() + state.pushStack(state.stackTypes.last) } def duplicateTopTwo(state: MethodGeneratorState) = { state.methodVisitor.visitInsn(DUP2) - state.pushStack() - state.pushStack() + state.pushStack(state.stackTypes.last) + state.pushStack(state.stackTypes.last) } def loadThis(state: MethodGeneratorState) = { state.methodVisitor.visitVarInsn(ALOAD, 0) - state.pushStack() + state.pushStack(UserType(javaifyClass(state.className))) } - def loadClass(className: String, state: MethodGeneratorState) = { - state.methodVisitor.visitVarInsn - } + // def loadClass(className: String, state: MethodGeneratorState) = { + // state.methodVisitor.visitVarInsn + // } def storeField(name: String, fieldType: Type, state: MethodGeneratorState) = { - state.methodVisitor.visitFieldInsn(PUTFIELD, javaifyClass(state.className), name, asmType(fieldType)) - state.popStack(1) + state.methodVisitor.visitFieldInsn(PUTFIELD, javaifyClass(state.className), name, javaSignature(fieldType)) + state.popStack() } /** @@ -106,13 +120,13 @@ private object Instructions { * @param state */ def loadField(name: String, fieldType: Type, state: MethodGeneratorState) = { - state.methodVisitor.visitFieldInsn(GETFIELD, javaifyClass(state.className), name, asmType(fieldType)) + state.methodVisitor.visitFieldInsn(GETFIELD, javaifyClass(state.className), name, javaSignature(fieldType)) // object is popped and field is pushed } def binaryOperation(opcode: Int, state: MethodGeneratorState): Unit = { state.methodVisitor.visitInsn(opcode) - state.popStack(1) // the instruction takes two arguments from the stack and then pushes the result + state.popStack() // the instruction takes two arguments from the stack and then pushes the result } def callMethod( @@ -132,12 +146,13 @@ private object Instructions { def returnType(descriptor: Type, state: MethodGeneratorState): Unit = { state.methodVisitor.visitInsn(asmReturnCode(descriptor)) - state.popStack(1) + state.popStack() } def newObject(className: String, state: MethodGeneratorState): Unit = { - state.methodVisitor.visitTypeInsn(NEW, javaifyClass(className)) - state.pushStack() + val javaifiedClass = javaifyClass(className) + state.methodVisitor.visitTypeInsn(NEW, javaifiedClass) + state.pushStack(UserType(javaifiedClass)) } def callConstructor(className: String, parameterDescriptors: List[Type], state: MethodGeneratorState): Unit = { diff --git a/src/main/scala/de/students/ByteCodeGenerator/OPCodes.scala b/src/main/scala/de/students/ByteCodeGenerator/OPCodes.scala new file mode 100644 index 0000000..8633b92 --- /dev/null +++ b/src/main/scala/de/students/ByteCodeGenerator/OPCodes.scala @@ -0,0 +1,90 @@ +package de.students.ByteCodeGenerator + +import org.objectweb.asm.Opcodes.* +import de.students.Parser.* + +private def asmLoadInsn(t: Type): Int = try { + stringToOpcode(typePrefix(t) + "LOAD") +} catch { + case ByteCodeGeneratorException(_) => throw ByteCodeGeneratorException(f"type $t can not be fetched as variable") +} + +private def asmStoreInsn(t: Type): Int = try { + stringToOpcode(typePrefix(t) + "STORE") +} catch { + case ByteCodeGeneratorException(_) => throw ByteCodeGeneratorException(f"type $t can not be saved as variable") +} + +private def asmReturnCode(t: Type): Int = try { + stringToOpcode(typePrefix(t) + "RETURN") +} catch { + case ByteCodeGeneratorException(_) => throw ByteCodeGeneratorException(f"type $t is not allowed as return type") +} + +private def typePrefix(t: Type): String = t match { + case IntType | CharType | ShortType | ByteType => "I" + case BoolType => "I" // TODO should there be a specific opcode? + case LongType => "L" + case FloatType => "F" + case DoubleType => "D" + case ArrayType(_) | UserType(_) => "A" + case _ => throw ByteCodeGeneratorException(f"the type $t is not allowed") +} + +private def binaryOpName(op: String): String = op match { + case "+" => "ADD" + case "-" => "SUB" + case "*" => "MUL" + case "/" => "DIV" + case "%" => "REM" + case _ => throw ByteCodeGeneratorException(f"the operator \"$op\" is not allowed for binary operations") +} + +private def binaryOpcode(op: String, t: Type): Int = stringToOpcode(typePrefix(t) + binaryOpName(op)) + +private def stringToOpcode(name: String): Int = name match { + case "ILOAD" => ILOAD + case "LLOAD" => LLOAD + case "FLOAD" => FLOAD + case "DLOAD" => DLOAD + case "ALOAD" => ALOAD + + case "ISTORE" => ISTORE + case "LSTORE" => LSTORE + case "FSTORE" => FSTORE + case "DSTORE" => DSTORE + case "ASTORE" => ASTORE + + case "IRETURN" => IRETURN + case "LRETURN" => LRETURN + case "FRETURN" => FRETURN + case "DRETURN" => DRETURN + case "ARETURN" => ARETURN + + case "IADD" => IADD + case "LADD" => LADD + case "FADD" => FADD + case "DADD" => DADD + + case "ISUB" => ISUB + case "LSUB" => LSUB + case "FSUB" => FSUB + case "DSUB" => DSUB + + case "IMUL" => IMUL + case "LMUL" => LMUL + case "FMUL" => FMUL + case "DMUL" => DMUL + + case "IDIV" => IDIV + case "LDIV" => LDIV + case "FDIV" => FDIV + case "DDIV" => DDIV + + case "IREM" => IREM + case "LREM" => LREM + case "FREM" => FREM + case "DREM" => DREM + + case _ => throw ByteCodeGeneratorException(f"opcode $name not recognized") +} diff --git a/src/main/scala/de/students/ByteCodeGenerator/Statements.scala b/src/main/scala/de/students/ByteCodeGenerator/Statements.scala index a24a6e4..529a024 100644 --- a/src/main/scala/de/students/ByteCodeGenerator/Statements.scala +++ b/src/main/scala/de/students/ByteCodeGenerator/Statements.scala @@ -175,6 +175,6 @@ private def generateExpressionStatement(statement: StatementExpression, state: M // expression result is not used, so the stack must be popped if (t != VoidType) { - Instructions.pop(state) + Instructions.popType(state) } } diff --git a/src/main/scala/de/students/Parser/ASTBuilder.scala b/src/main/scala/de/students/Parser/ASTBuilder.scala index b52e3eb..fb54085 100644 --- a/src/main/scala/de/students/Parser/ASTBuilder.scala +++ b/src/main/scala/de/students/Parser/ASTBuilder.scala @@ -418,7 +418,8 @@ object ASTBuilder { Literal(ctx.INTEGER_LITERAL().getText.toInt) } else if (ctx.STRING_LITERAL() != null) { // Handle string literals - Literal(ctx.STRING_LITERAL().getText) + val stringLiteralText = ctx.STRING_LITERAL().getText + Literal(stringLiteralText.substring(1, stringLiteralText.length - 1)) } else if (ctx.BOOLEAN_LITERAL() != null) { // Handle boolean literals (true/false) ctx.BOOLEAN_LITERAL().getText match { @@ -442,8 +443,15 @@ object ASTBuilder { // Handle double literals Literal(ctx.DOUBLE_LITERAL().getText.toDouble) // Double literals may have 'd'/'D' suffix which is optional } else if (ctx.CHAR_LITERAL() != null) { - // Handle double literals - Literal(ctx.CHAR_LITERAL().getText) // Char Literal + // Handle char literals + if (ctx.CHAR_LITERAL().getText.length != 3) { + throw new UnsupportedOperationException( + s"Char literals must specify exactly one character, but found: ${ctx.getText}" + ) + } + Literal( + ctx.CHAR_LITERAL().getText.charAt(1) + ) // Char Literals are only the one character between two single quotes } else { // If literal is not recognized throw new UnsupportedOperationException(s"Unsupported literal: ${ctx.getText}") @@ -487,4 +495,4 @@ object ASTBuilder { } } -} +} \ No newline at end of file