From 0692a7fcd1c6fefae83cf664e19d9c7147e99276 Mon Sep 17 00:00:00 2001 From: angelos sfyrakis Date: Wed, 23 Sep 2026 19:24:19 +0300 Subject: [PATCH] Fix type checking and codegen bugs, add Docker and test suite Type checking: - Reject duplicate classes, fields, methods, parameters and local variables - Allow returning a subclass of the declared return type - Check local variable and field types, println arguments and integer literal range - Report undefined identifiers and use of main's argument instead of crashing - Compare strings with equals() instead of == Code generation: - Support boolean array allocation and convert boolean elements to/from i32 - Emit a vtable for the main class; main no longer takes a vtable slot Main exits with status 1 on errors and only writes the .ll file after type checking. Add a Dockerfile, run_tests.sh (compares compiled programs against javac/java), a GitHub Actions workflow, and an updated README. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 3 + .gitattributes | 4 + .github/workflows/tests.yml | 15 + .gitignore | 10 + CheckingVisitor.java | 113 +++-- Dockerfile | 14 + Example.ll | 534 +++++++++++------------- LLVMVisitor.java | 33 +- Main.java | 66 ++- MiniJavaClass.java | 11 +- MyVisitor.java | 7 +- README.md | 102 ++++- SymbolTable.java | 68 ++- run_tests.sh | 87 ++++ test/typechecking/Add (copy).java | 5 - test/typechecking/Add (copy).out | 0 test/typechecking/ArrayTest (copy).java | 25 -- test/typechecking/ArrayTest (copy).out | 1 - 18 files changed, 660 insertions(+), 438 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 run_tests.sh delete mode 100644 test/typechecking/Add (copy).java delete mode 100644 test/typechecking/Add (copy).out delete mode 100644 test/typechecking/ArrayTest (copy).java delete mode 100644 test/typechecking/ArrayTest (copy).out diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f74c19f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +*.class +*.ll diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..254f0f1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto +*.sh text eol=lf +Makefile text eol=lf +*.jar binary diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a5922fd --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,15 @@ +name: tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build compiler image + run: docker build -t minijava . + - name: Run test suite + run: docker run --rm minijava diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb27ac1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build output +*.class + +# Generated by JTB / JavaCC during `make compile` +syntaxtree/ +visitor/ +MiniJavaParser*.java +Token*.java +ParseException.java +JavaCharStream.java diff --git a/CheckingVisitor.java b/CheckingVisitor.java index 9d32b28..1dc2511 100644 --- a/CheckingVisitor.java +++ b/CheckingVisitor.java @@ -58,6 +58,7 @@ public String visit(ClassDeclaration n, Void argu) throws Exception { String classname = n.f1.accept(this, null); this.inClass = false; Main.ST.enterClass(classname); + n.f3.accept(this, null); n.f4.accept(this, null); return null; } @@ -79,6 +80,7 @@ public String visit(ClassExtendsDeclaration n, Void argu) throws Exception { String parentclassName = n.f3.accept(this, null);; this.inClass = false; Main.ST.enterClass(classname); + n.f5.accept(this, null); n.f6.accept(this, null); return null; } @@ -107,11 +109,13 @@ public String visit(MethodDeclaration n, Void argu) throws Exception { String myName = n.f2.accept(this, null); this.inClass = false; Main.ST.enterMethod(myName); - String exprType = n.f10.accept(this, null); + n.f7.accept(this, null); n.f8.accept(this, null); - if (!myType.equals(exprType)){ + String exprType = n.f10.accept(this, null); + // the returned value must have the declared type or be a subclass of it + if (!myType.equals(exprType) && !Main.ST.childHasParent(exprType, myType)){ throw new Exception("MethodDeclaration: False return type"+"("+Main.ST.getCurrentMethodName()+")"); - } // return statement bust be the same as the type + } Main.ST.exitMethod(); return null; } @@ -220,7 +224,22 @@ public String visit(IntegerType n, Void argu) { */ public boolean isObject(String idType){ - return idType != "int" && idType != "int[]" && idType != "boolean" && idType != "boolean[]"; + return !idType.equals("int") && !idType.equals("int[]") && !idType.equals("boolean") && !idType.equals("boolean[]"); + } + + /** + * f0 -> "System.out.println" + * f1 -> "(" + * f2 -> Expression() + * f3 -> ")" + * f4 -> ";" + */ + @Override + public String visit(PrintStatement n, Void argu) throws Exception { + if (!"int".equals(n.f2.accept(this, argu))){ + throw new Exception("PrintStatement: only integers can be printed"+"("+Main.ST.getCurrentMethodName()+")"); + } + return null; } @@ -240,7 +259,7 @@ public String visit(AssignmentStatement n, Void argu) throws Exception { throw new Exception("AssignmentStatement: id is object but expr is not"+"("+Main.ST.getCurrentMethodName()+")"); } }else{ - if (idType != exprType){ + if (!idType.equals(exprType)){ throw new Exception("AssignmentStatement: id and expr not the same type"+"("+Main.ST.getCurrentMethodName()+")"); } } @@ -260,9 +279,9 @@ public String visit(AssignmentStatement n, Void argu) throws Exception { * f6 -> ";" */ private String arrayType(String type){ - if (type =="boolean[]"){ + if ("boolean[]".equals(type)){ return "boolean"; - }else if (type == "int[]"){ + }else if ("int[]".equals(type)){ return "int"; } return null; @@ -274,15 +293,15 @@ public String visit(ArrayAssignmentStatement n, Void argu) throws Exception { String _ret=null; String idType = n.f0.accept(this, argu); n.f1.accept(this, argu); - if (n.f2.accept(this, argu) != "int"){ + if (!"int".equals(n.f2.accept(this, argu))){ throw new Exception("ArrayAssignmentStatement: expr index is not an integer"+"("+Main.ST.getCurrentMethodName()+")"); } String exprType = n.f5.accept(this, argu); // its type must be the same as the typeOfArray String typeOfArray = arrayType(idType); if (typeOfArray == null){ - throw new Exception("ArrayAssignmentStatement: ID IS NOT AN ARRAY"+"("+Main.ST.getCurrentMethodName()+")"); + throw new Exception("ArrayAssignmentStatement: id is not an array"+"("+Main.ST.getCurrentMethodName()+")"); }else{ - if (exprType != typeOfArray){ + if (!typeOfArray.equals(exprType)){ throw new Exception("ArrayAssignmentStatement: id and expr not the same type"+"("+Main.ST.getCurrentMethodName()+")"); } return typeOfArray; @@ -303,7 +322,7 @@ public String visit(IfStatement n, Void argu) throws Exception { String _ret=null; n.f0.accept(this, argu); n.f1.accept(this, argu); - if (n.f2.accept(this, argu)!="boolean"){ + if (!"boolean".equals(n.f2.accept(this, argu))){ throw new Exception("Not a boolean in if expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f3.accept(this, argu); @@ -325,7 +344,7 @@ public String visit(WhileStatement n, Void argu) throws Exception { String _ret=null; n.f0.accept(this, argu); n.f1.accept(this, argu); - if (n.f2.accept(this, argu)!="boolean"){ + if (!"boolean".equals(n.f2.accept(this, argu))){ throw new Exception("Not a boolean in while expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f3.accept(this, argu); @@ -340,12 +359,12 @@ public String visit(WhileStatement n, Void argu) throws Exception { @Override public String visit(AndExpression n, Void argu) throws Exception { String _ret=null; - if (n.f0.accept(this, argu) !="boolean"){ - throw new Exception("Not a boolean in end expression"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"boolean".equals(n.f0.accept(this, argu))){ + throw new Exception("Not a boolean in AND expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (n.f2.accept(this, argu) !="boolean"){ - throw new Exception("Not a boolean in end expression"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"boolean".equals(n.f2.accept(this, argu))){ + throw new Exception("Not a boolean in AND expression"+"("+Main.ST.getCurrentMethodName()+")"); } return "boolean"; } @@ -358,11 +377,11 @@ public String visit(AndExpression n, Void argu) throws Exception { @Override public String visit(CompareExpression n, Void argu) throws Exception { String _ret=null; - if (n.f0.accept(this, argu) != "int"){ + if (!"int".equals(n.f0.accept(this, argu))){ throw new Exception("Not an integer in LESS expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (n.f2.accept(this, argu) != "int"){ + if (!"int".equals(n.f2.accept(this, argu))){ throw new Exception("Not an integer in LESS expression"+"("+Main.ST.getCurrentMethodName()+")"); } return "boolean"; @@ -378,11 +397,11 @@ public String visit(PlusExpression n, Void argu) throws Exception { String _ret=null; String f0Str = n.f0.accept(this, argu); String f2Str = n.f2.accept(this, argu); - if (f0Str != "int"){ + if (!"int".equals(f0Str)){ throw new Exception("Not an integer in PLUS expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (f2Str != "int"){ + if (!"int".equals(f2Str)){ throw new Exception("Not an integer in PLUS expression"+"("+Main.ST.getCurrentMethodName()+")"); } return "int"; @@ -395,11 +414,11 @@ public String visit(PlusExpression n, Void argu) throws Exception { @Override public String visit(MinusExpression n, Void argu) throws Exception { String _ret=null; - if (n.f0.accept(this, argu) != "int"){ + if (!"int".equals(n.f0.accept(this, argu))){ throw new Exception("Not an integer in MINUS expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (n.f2.accept(this, argu) != "int"){ + if (!"int".equals(n.f2.accept(this, argu))){ throw new Exception("Not an integer in MINUS expression"+"("+Main.ST.getCurrentMethodName()+")"); } return "int"; @@ -413,11 +432,11 @@ public String visit(MinusExpression n, Void argu) throws Exception { @Override public String visit(TimesExpression n, Void argu) throws Exception { String _ret=null; - if (n.f0.accept(this, argu) != "int"){ + if (!"int".equals(n.f0.accept(this, argu))){ throw new Exception("Not an integer in TIMES expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (n.f2.accept(this, argu) != "int"){ + if (!"int".equals(n.f2.accept(this, argu))){ throw new Exception("Not an integer in TIMES expression"+"("+Main.ST.getCurrentMethodName()+")"); } return "int"; @@ -433,14 +452,15 @@ public String visit(TimesExpression n, Void argu) throws Exception { public String visit(ArrayLookup n, Void argu) throws Exception { String _ret=null; String arrayType = n.f0.accept(this, argu); - if (arrayType != "boolean[]" && arrayType != "int[]"){ - throw new Exception("Not an array identigier in ArrayLookup expression"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"boolean[]".equals(arrayType) && !"int[]".equals(arrayType)){ + throw new Exception("Not an array in ArrayLookup expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); - if (n.f2.accept(this, argu) != "int"){ + if (!"int".equals(n.f2.accept(this, argu))){ throw new Exception("Not an integer in ArrayLookup expression"+"("+Main.ST.getCurrentMethodName()+")"); } - if (arrayType=="int[]") return "int"; + Main.ST.setArrayLookupType(n, arrayType); + if ("int[]".equals(arrayType)) return "int"; return "boolean"; } @@ -453,8 +473,8 @@ public String visit(ArrayLookup n, Void argu) throws Exception { public String visit(ArrayLength n, Void argu) throws Exception { String _ret=null; String arrayType = n.f0.accept(this, argu); - if (arrayType != "int[]" && arrayType != "boolean[]"){ - throw new Exception("Not an array id in ArrayLength expression"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"int[]".equals(arrayType) && !"boolean[]".equals(arrayType)){ + throw new Exception("Not an array in ArrayLength expression"+"("+Main.ST.getCurrentMethodName()+")"); } n.f1.accept(this, argu); n.f2.accept(this, argu); @@ -557,7 +577,11 @@ public String visit(PrimaryExpression n, Void argu) throws Exception { */ @Override public String visit(IntegerLiteral n, Void argu) throws Exception { - n.f0.accept(this, argu); + try { + Integer.parseInt(n.f0.toString()); + } catch (NumberFormatException ex) { + throw new Exception("IntegerLiteral: " + n.f0.toString() + " is out of range"+"("+Main.ST.getCurrentMethodName()+")"); + } return "int"; } @@ -601,8 +625,8 @@ public String visit(ThisExpression n, Void argu) throws Exception { public String visit(IntegerArrayAllocationExpression n, Void argu) throws Exception { String _ret=null; String f3type = n.f3.accept(this, argu); - if (f3type != "int"){ - throw new Exception("Array size is not na integer"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"int".equals(f3type)){ + throw new Exception("Array size is not an integer"+"("+Main.ST.getCurrentMethodName()+")"); } return "int[]"; } @@ -617,8 +641,8 @@ public String visit(IntegerArrayAllocationExpression n, Void argu) throws Except public String visit(BooleanArrayAllocationExpression n, Void argu) throws Exception { String _ret=null; String f3type = n.f3.accept(this, argu); - if (f3type != "int"){ - throw new Exception("Array size is not na integer"+"("+Main.ST.getCurrentMethodName()+")"); + if (!"int".equals(f3type)){ + throw new Exception("Array size is not an integer"+"("+Main.ST.getCurrentMethodName()+")"); } return "boolean[]"; } @@ -639,7 +663,7 @@ public String visit(AllocationExpression n, Void argu) throws Exception { String className = n.f1.accept(this, argu); this.inClass = false; if (!Main.ST.existsClass(className)){ - throw new Exception("Class+ "+className+" does not exist"+"("+Main.ST.getCurrentMethodName()+")"); + throw new Exception("Class "+className+" does not exist"+"("+Main.ST.getCurrentMethodName()+")"); } return className; } @@ -649,10 +673,17 @@ public String visit(AllocationExpression n, Void argu) throws Exception { @Override public String visit(Identifier n, Void argu) throws Exception { String idName = n.f0.toString(); - if (this.inClass == false){ - return Main.ST.lookup(idName); - } - return idName; + if (this.inClass){ + return idName; + } + String type = Main.ST.lookup(idName); + if (type == null){ + throw new Exception("Undefined identifier: " + idName + "("+Main.ST.getCurrentMethodName()+")"); + } + if (type.equals(SymbolTable.MAIN_ARGS_TYPE)){ + throw new Exception("The argument of main cannot be used: " + idName + "("+Main.ST.getCurrentMethodName()+")"); + } + return type; } /** @@ -663,7 +694,7 @@ public String visit(Identifier n, Void argu) throws Exception { public String visit(NotExpression n, Void argu) throws Exception { String _ret=null; n.f0.accept(this, argu); - if (n.f1.accept(this, argu) != "boolean"){ + if (!"boolean".equals(n.f1.accept(this, argu))){ throw new Exception("NotExpression: primary expression is not a boolean"+"("+Main.ST.getCurrentMethodName()+")"); } return "boolean"; diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3bd60ed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Builds the MiniJava compiler and includes clang to compile/run the generated LLVM IR. +# clang 14 (Ubuntu 22.04) is used because the generated IR uses typed pointers (i8*), +# which newer LLVM versions no longer support. +FROM eclipse-temurin:17-jdk-jammy + +RUN apt-get update \ + && apt-get install -y --no-install-recommends clang make \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . . +RUN make compile + +CMD ["./run_tests.sh"] diff --git a/Example.ll b/Example.ll index 67a8374..dccef7d 100644 --- a/Example.ll +++ b/Example.ll @@ -1,7 +1,11 @@ -@.LinearSearch_vtable = global [0 x i8*] [] -@.LS_vtable = global [4 x i8*] [i8* bitcast (i32 (i8*,i32)* @LS.Start to i8*), i8* bitcast (i32 (i8*)* @LS.Print to i8*), i8* bitcast (i32 (i8*,i32)* @LS.Search to i8*), i8* bitcast (i32 (i8*,i32)* @LS.Init to i8*)] - - +@.LS_vtable = global [4 x i8*] [ + i8* bitcast (i32 (i8* , i32)* @LS.Start to i8*), + i8* bitcast (i32 (i8* )* @LS.Print to i8*), + i8* bitcast (i32 (i8* , i32)* @LS.Search to i8*), + i8* bitcast (i32 (i8* , i32)* @LS.Init to i8*) +] +@.LinearSearch_vtable = global [0 x i8*] [ +] declare i8* @calloc(i32, i32) declare i32 @printf(i8*, ...) declare void @exit(i32) @@ -22,19 +26,19 @@ define void @throw_oob() { } define i32 @main() { + %_0 = call i8* @calloc(i32 1, i32 20) %_1 = bitcast i8* %_0 to i8*** %_2 = getelementptr [4 x i8*], [4 x i8*]* @.LS_vtable, i32 0, i32 0 store i8** %_2, i8*** %_1 - ; LS.Start : 0 %_3 = bitcast i8* %_0 to i8*** %_4 = load i8**, i8*** %_3 %_5 = getelementptr i8*, i8** %_4, i32 0 %_6 = load i8*, i8** %_5 - %_7 = bitcast i8* %_6 to i32 (i8*,i32)* + %_7 = bitcast i8* %_6 to i32(i8*, i32)* %_8 = call i32 %_7(i8* %_0, i32 10) call void (i32) @print_int(i32 %_8) - + ret i32 0 } @@ -42,326 +46,280 @@ define i32 @LS.Start(i8* %this, i32 %.sz) { %sz = alloca i32 store i32 %.sz, i32* %sz %aux01 = alloca i32 - %aux02 = alloca i32 - - ; LS.Init : 3 - %_0 = bitcast i8* %this to i8*** - %_1 = load i8**, i8*** %_0 - %_2 = getelementptr i8*, i8** %_1, i32 3 - %_3 = load i8*, i8** %_2 - %_4 = bitcast i8* %_3 to i32 (i8*,i32)* - %_6 = load i32, i32* %sz - %_5 = call i32 %_4(i8* %this, i32 %_6) - store i32 %_5, i32* %aux01 - - ; LS.Print : 1 - %_7 = bitcast i8* %this to i8*** - %_8 = load i8**, i8*** %_7 - %_9 = getelementptr i8*, i8** %_8, i32 1 - %_10 = load i8*, i8** %_9 - %_11 = bitcast i8* %_10 to i32 (i8*)* - %_12 = call i32 %_11(i8* %this) - store i32 %_12, i32* %aux02 - + %_9 = bitcast i8* %this to i8*** + %_10 = load i8**, i8*** %_9 + %_11 = getelementptr i8*, i8** %_10, i32 3 + %_12 = load i8*, i8** %_11 + %_13 = bitcast i8* %_12 to i32(i8*, i32)* + %_15 = load i32, i32* %sz + %_14 = call i32 %_13(i8* %this, i32 %_15) + store i32 %_14, i32* %aux01 + %_16 = bitcast i8* %this to i8*** + %_17 = load i8**, i8*** %_16 + %_18 = getelementptr i8*, i8** %_17, i32 1 + %_19 = load i8*, i8** %_18 + %_20 = bitcast i8* %_19 to i32(i8*)* + %_21 = call i32 %_20(i8* %this) + store i32 %_21, i32* %aux02 call void (i32) @print_int(i32 9999) - - ; LS.Search : 2 - %_13 = bitcast i8* %this to i8*** - %_14 = load i8**, i8*** %_13 - %_15 = getelementptr i8*, i8** %_14, i32 2 - %_16 = load i8*, i8** %_15 - %_17 = bitcast i8* %_16 to i32 (i8*,i32)* - %_18 = call i32 %_17(i8* %this, i32 8) - call void (i32) @print_int(i32 %_18) - - ; LS.Search : 2 - %_19 = bitcast i8* %this to i8*** - %_20 = load i8**, i8*** %_19 - %_21 = getelementptr i8*, i8** %_20, i32 2 - %_22 = load i8*, i8** %_21 - %_23 = bitcast i8* %_22 to i32 (i8*,i32)* - %_24 = call i32 %_23(i8* %this, i32 12) - call void (i32) @print_int(i32 %_24) - - ; LS.Search : 2 - %_25 = bitcast i8* %this to i8*** - %_26 = load i8**, i8*** %_25 - %_27 = getelementptr i8*, i8** %_26, i32 2 - %_28 = load i8*, i8** %_27 - %_29 = bitcast i8* %_28 to i32 (i8*,i32)* - %_30 = call i32 %_29(i8* %this, i32 17) - call void (i32) @print_int(i32 %_30) - - ; LS.Search : 2 - %_31 = bitcast i8* %this to i8*** - %_32 = load i8**, i8*** %_31 - %_33 = getelementptr i8*, i8** %_32, i32 2 - %_34 = load i8*, i8** %_33 - %_35 = bitcast i8* %_34 to i32 (i8*,i32)* - %_36 = call i32 %_35(i8* %this, i32 50) - call void (i32) @print_int(i32 %_36) - - ret i32 55 + %_22 = bitcast i8* %this to i8*** + %_23 = load i8**, i8*** %_22 + %_24 = getelementptr i8*, i8** %_23, i32 2 + %_25 = load i8*, i8** %_24 + %_26 = bitcast i8* %_25 to i32(i8*, i32)* + %_27 = call i32 %_26(i8* %this, i32 8) + call void (i32) @print_int(i32 %_27) + %_28 = bitcast i8* %this to i8*** + %_29 = load i8**, i8*** %_28 + %_30 = getelementptr i8*, i8** %_29, i32 2 + %_31 = load i8*, i8** %_30 + %_32 = bitcast i8* %_31 to i32(i8*, i32)* + %_33 = call i32 %_32(i8* %this, i32 12) + call void (i32) @print_int(i32 %_33) + %_34 = bitcast i8* %this to i8*** + %_35 = load i8**, i8*** %_34 + %_36 = getelementptr i8*, i8** %_35, i32 2 + %_37 = load i8*, i8** %_36 + %_38 = bitcast i8* %_37 to i32(i8*, i32)* + %_39 = call i32 %_38(i8* %this, i32 17) + call void (i32) @print_int(i32 %_39) + %_40 = bitcast i8* %this to i8*** + %_41 = load i8**, i8*** %_40 + %_42 = getelementptr i8*, i8** %_41, i32 2 + %_43 = load i8*, i8** %_42 + %_44 = bitcast i8* %_43 to i32(i8*, i32)* + %_45 = call i32 %_44(i8* %this, i32 50) + call void (i32) @print_int(i32 %_45) + + ret i32 55 } define i32 @LS.Print(i8* %this) { %j = alloca i32 - store i32 1, i32* %j - + br label %loop0 loop0: - %_3 = load i32, i32* %j - %_4 = getelementptr i8, i8* %this, i32 16 - %_5 = bitcast i8* %_4 to i32* - %_6 = load i32, i32* %_5 - %_7 = icmp slt i32 %_3, %_6 - br i1 %_7, label %loop1, label %loop2 + %_46 = load i32, i32* %j + %_47 = getelementptr i8, i8* %this, i32 16 + %_48 = bitcast i8* %_47 to i32* + %_49 = load i32, i32* %_48 + %_50 = icmp slt i32 %_46, %_49 + br i1 %_50, label %loop1, label %loop2 loop1: - %_17 = getelementptr i8, i8* %this, i32 8 - %_18 = bitcast i8* %_17 to i32** - %_19 = load i32*, i32** %_18 - %_20 = load i32, i32* %j - %_8 = load i32, i32 *%_19 - %_9 = icmp ult i32 %_20, %_8 - br i1 %_9, label %oob14, label %oob15 - -oob14: - %_10 = add i32 %_20, 1 - %_11 = getelementptr i32, i32* %_19, i32 %_10 - %_12 = load i32, i32* %_11 - br label %oob16 - -oob15: - call void @throw_oob() - br label %oob16 - -oob16: - call void (i32) @print_int(i32 %_12) - - %_21 = load i32, i32* %j - %_22 = add i32 %_21, 1 - store i32 %_22, i32* %j - - - br label %loop0 + %_56 = getelementptr i8, i8* %this, i32 8 + %_57 = bitcast i8* %_56 to i32** + %_58 = load i32*, i32** %_57 + %_59 = load i32, i32* %j + %_51 = load i32, i32* %_58 + %_53 = icmp ult i32 %_59, %_51 + br i1 %_53, label %oob0, label %oob1 + +oob0: + %_52 = add i32 %_59, 1 + %_54 = getelementptr i32, i32* %_58, i32 %_52 + %_55 = load i32, i32* %_54 + br label %oob2 + +oob1: + call void @throw_oob() + br label %oob2 + +oob2: + call void (i32) @print_int(i32 %_55) + %_60 = load i32, i32* %j + %_61 = add i32 %_60, 1 + store i32 %_61, i32* %j + + br label %loop0 loop2: - - ret i32 0 + + ret i32 0 } define i32 @LS.Search(i8* %this, i32 %.num) { %num = alloca i32 store i32 %.num, i32* %num %j = alloca i32 - %ls01 = alloca i1 - %ifound = alloca i32 - %aux01 = alloca i32 - %aux02 = alloca i32 - %nt = alloca i32 - store i32 1, i32* %j - store i1 0, i1* %ls01 - store i32 0, i32* %ifound - - br label %loop0 -loop0: - %_3 = load i32, i32* %j - %_4 = getelementptr i8, i8* %this, i32 16 - %_5 = bitcast i8* %_4 to i32* - %_6 = load i32, i32* %_5 - %_7 = icmp slt i32 %_3, %_6 - br i1 %_7, label %loop1, label %loop2 + br label %loop3 + +loop3: + %_62 = load i32, i32* %j + %_63 = getelementptr i8, i8* %this, i32 16 + %_64 = bitcast i8* %_63 to i32* + %_65 = load i32, i32* %_64 + %_66 = icmp slt i32 %_62, %_65 + br i1 %_66, label %loop4, label %loop5 + +loop4: + %_72 = getelementptr i8, i8* %this, i32 8 + %_73 = bitcast i8* %_72 to i32** + %_74 = load i32*, i32** %_73 + %_75 = load i32, i32* %j + %_67 = load i32, i32* %_74 + %_69 = icmp ult i32 %_75, %_67 + br i1 %_69, label %oob3, label %oob4 + +oob3: + %_68 = add i32 %_75, 1 + %_70 = getelementptr i32, i32* %_74, i32 %_68 + %_71 = load i32, i32* %_70 + br label %oob5 + +oob4: + call void @throw_oob() + br label %oob5 -loop1: - %_17 = getelementptr i8, i8* %this, i32 8 - %_18 = bitcast i8* %_17 to i32** - %_19 = load i32*, i32** %_18 - %_20 = load i32, i32* %j - %_8 = load i32, i32 *%_19 - %_9 = icmp ult i32 %_20, %_8 - br i1 %_9, label %oob14, label %oob15 - -oob14: - %_10 = add i32 %_20, 1 - %_11 = getelementptr i32, i32* %_19, i32 %_10 - %_12 = load i32, i32* %_11 - br label %oob16 - -oob15: - call void @throw_oob() - br label %oob16 - -oob16: - store i32 %_12, i32* %aux01 - - %_21 = load i32, i32* %num - %_22 = add i32 %_21, 1 - store i32 %_22, i32* %aux02 - - %_26 = load i32, i32* %aux01 - %_27 = load i32, i32* %num - %_28 = icmp slt i32 %_26, %_27 - br i1 %_28, label %if23, label %if24 - -if23: - store i32 0, i32* %nt - - br label %if25 - -if24: - - %_33 = load i32, i32* %aux01 - %_34 = load i32, i32* %aux02 - %_35 = icmp slt i32 %_33, %_34 - %_32 = xor i1 1, %_35 - br i1 %_32, label %if29, label %if30 - -if29: - store i32 0, i32* %nt - - br label %if31 - -if30: - - store i1 1, i1* %ls01 - - store i32 1, i32* %ifound - - %_36 = getelementptr i8, i8* %this, i32 16 - %_37 = bitcast i8* %_36 to i32* - %_38 = load i32, i32* %_37 - store i32 %_38, i32* %j - - - br label %if31 - -if31: - - br label %if25 - -if25: - - %_39 = load i32, i32* %j - %_40 = add i32 %_39, 1 - store i32 %_40, i32* %j - - - br label %loop0 +oob5: + store i32 %_71, i32* %aux01 + %_76 = load i32, i32* %num + %_77 = add i32 %_76, 1 + store i32 %_77, i32* %aux02 + %_78 = load i32, i32* %aux01 + %_79 = load i32, i32* %num + %_80 = icmp slt i32 %_78, %_79 + br i1 %_80, label %if0, label %if1 -loop2: - - %_41 = load i32, i32* %ifound - ret i32 %_41 +if0: + store i32 0, i32* %nt + + br label %if2 + +if1: + %_81 = load i32, i32* %aux01 + %_82 = load i32, i32* %aux02 + %_83 = icmp slt i32 %_81, %_82 + %_84 = xor i1 1, %_83 + br i1 %_84, label %if3, label %if4 + +if3: + store i32 0, i32* %nt + + br label %if5 + +if4: + store i1 1, i1* %ls01 + store i32 1, i32* %ifound + %_85 = getelementptr i8, i8* %this, i32 16 + %_86 = bitcast i8* %_85 to i32* + %_87 = load i32, i32* %_86 + store i32 %_87, i32* %j + + br label %if5 + +if5: + + br label %if2 + +if2: + %_88 = load i32, i32* %j + %_89 = add i32 %_88, 1 + store i32 %_89, i32* %j + + br label %loop3 + +loop5: + %_90 = load i32, i32* %ifound + + ret i32 %_90 } define i32 @LS.Init(i8* %this, i32 %.sz) { %sz = alloca i32 store i32 %.sz, i32* %sz %j = alloca i32 - %k = alloca i32 - %aux01 = alloca i32 - %aux02 = alloca i32 - - %_0 = load i32, i32* %sz - %_1 = getelementptr i8, i8* %this, i32 16 - %_2 = bitcast i8* %_1 to i32* - store i32 %_0, i32* %_2 - - %_9 = load i32, i32* %sz - %_6 = icmp slt i32 %_9, 0 - br i1 %_6, label %arr_alloc7, label %arr_alloc8 - -arr_alloc7: + %_91 = getelementptr i8, i8* %this, i32 16 + %_92 = bitcast i8* %_91 to i32* + %_93 = load i32, i32* %sz + store i32 %_93, i32* %_92 + %_94 = getelementptr i8, i8* %this, i32 8 + %_95 = bitcast i8* %_94 to i32** + %_100 = load i32, i32* %sz + %_96 = icmp slt i32 %_100, 0 + br i1 %_96, label %arr_alloc0, label %arr_alloc1 + +arr_alloc0: call void @throw_oob() - br label %arr_alloc8 - -arr_alloc8: - %_3 = add i32 %_9, 1 - %_4 = call i8* @calloc(i32 4, i32 %_3) - %_5 = bitcast i8* %_4 to i32* - store i32 %_9, i32* %_5 - %_10 = getelementptr i8, i8* %this, i32 8 - %_11 = bitcast i8* %_10 to i32** - store i32* %_5, i32** %_11 - + br label %arr_alloc1 + +arr_alloc1: + %_97 = add i32 %_100, 1 + %_98 = call i8* @calloc(i32 4, i32 %_97) + %_99 = bitcast i8* %_98 to i32* + store i32 %_100, i32* %_99 + store i32* %_99, i32** %_95 store i32 1, i32* %j - - %_12 = getelementptr i8, i8* %this, i32 16 - %_13 = bitcast i8* %_12 to i32* - %_14 = load i32, i32* %_13 - %_15 = add i32 %_14, 1 - store i32 %_15, i32* %k - - br label %loop16 - -loop16: - %_19 = load i32, i32* %j - %_20 = getelementptr i8, i8* %this, i32 16 - %_21 = bitcast i8* %_20 to i32* - %_22 = load i32, i32* %_21 - %_23 = icmp slt i32 %_19, %_22 - br i1 %_23, label %loop17, label %loop18 - -loop17: - %_24 = load i32, i32* %j - %_25 = mul i32 2, %_24 - store i32 %_25, i32* %aux01 - - %_26 = load i32, i32* %k - %_27 = sub i32 %_26, 3 - store i32 %_27, i32* %aux02 - - %_36 = getelementptr i8, i8* %this, i32 8 - %_37 = bitcast i8* %_36 to i32** - %_38 = load i32*, i32** %_37 - %_39 = load i32, i32* %j - %_28 = load i32, i32 *%_38 - %_29 = icmp ult i32 %_39, %_28 - br i1 %_29, label %oob33, label %oob34 - -oob33: - %_30 = add i32 %_39, 1 - %_31 = getelementptr i32, i32* %_38, i32 %_30 - %_40 = load i32, i32* %aux01 - %_41 = load i32, i32* %aux02 - %_42 = add i32 %_40, %_41 - store i32 %_42, i32* %_31 - br label %oob35 - -oob34: - call void @throw_oob() - br label %oob35 - -oob35: - - %_43 = load i32, i32* %j - %_44 = add i32 %_43, 1 - store i32 %_44, i32* %j - - %_45 = load i32, i32* %k - %_46 = sub i32 %_45, 1 - store i32 %_46, i32* %k - - - br label %loop16 - -loop18: - - ret i32 0 -} \ No newline at end of file + %_101 = getelementptr i8, i8* %this, i32 16 + %_102 = bitcast i8* %_101 to i32* + %_103 = load i32, i32* %_102 + %_104 = add i32 %_103, 1 + store i32 %_104, i32* %k + + br label %loop6 + +loop6: + %_105 = load i32, i32* %j + %_106 = getelementptr i8, i8* %this, i32 16 + %_107 = bitcast i8* %_106 to i32* + %_108 = load i32, i32* %_107 + %_109 = icmp slt i32 %_105, %_108 + br i1 %_109, label %loop7, label %loop8 + +loop7: + %_110 = load i32, i32* %j + %_111 = mul i32 2, %_110 + store i32 %_111, i32* %aux01 + %_112 = load i32, i32* %k + %_113 = sub i32 %_112, 3 + store i32 %_113, i32* %aux02 + %_118 = getelementptr i8, i8* %this, i32 8 + %_119 = bitcast i8* %_118 to i32** + %_120 = load i32*, i32** %_119 + %_121 = load i32, i32* %j + %_114 = load i32, i32* %_120 + %_115 = icmp ult i32 %_121, %_114 + br i1 %_115, label %oob6, label %oob7 + +oob6: + %_122 = load i32, i32* %aux01 + %_123 = load i32, i32* %aux02 + %_124 = add i32 %_122, %_123 + %_116 = add i32 %_121, 1 + %_117 = getelementptr i32, i32* %_120, i32 %_116 + store i32 %_124, i32* %_117 + br label %oob8 + +oob7: + call void @throw_oob() + br label %oob8 + +oob8: + %_125 = load i32, i32* %j + %_126 = add i32 %_125, 1 + store i32 %_126, i32* %j + %_127 = load i32, i32* %k + %_128 = sub i32 %_127, 1 + store i32 %_128, i32* %k + + br label %loop6 + +loop8: + + ret i32 0 +} diff --git a/LLVMVisitor.java b/LLVMVisitor.java index 928935a..711ffdb 100644 --- a/LLVMVisitor.java +++ b/LLVMVisitor.java @@ -378,6 +378,12 @@ public String visit(ArrayAssignmentStatement n, Void argu) throws Exception { + assignmentLabelCode); // assignment String exprRegister= n.f5.accept(this, null); + if ("boolean[]".equals(idType)){ + // boolean elements are stored as i32 + String extendedRegister = getNewRegisterName(); + writeCode("\t" + extendedRegister + " = zext i1 " + exprRegister + " to i32\n"); + exprRegister = extendedRegister; + } String realIndexCode = "\t" + realIndexRegister + " = add i32 " + indexRegister + ", 1\n"; String getElementCode = "\t" + elementRegister + " = getelementptr i32, i32* " + arrayRegister + ", i32 " + realIndexRegister + "\n"; String storeExprCode = "\tstore i32 " + exprRegister + ", i32* " + elementRegister + "\n"; @@ -521,18 +527,35 @@ public String visit(AllocationExpression n, Void argu) throws Exception { @Override public String visit(IntegerArrayAllocationExpression n, Void argu) throws Exception { + return allocateArray(n.f3); + } + + /** + * f0 -> "new" + * f1 -> "boolean" + * f2 -> "[" + * f3 -> Expression() + * f4 -> "]" + */ + @Override + public String visit(BooleanArrayAllocationExpression n, Void argu) throws Exception { + // boolean arrays share the int array layout: i32 elements, size stored in element 0 + return allocateArray(n.f3); + } + + private String allocateArray(Node sizeExpression) throws Exception { // registers String compareRegister = getNewRegisterName(); String realSizeRegister = getNewRegisterName(); // we need one more element to store the size in order to check for array look ups String callocRegister = getNewRegisterName(); String castRegister = getNewRegisterName(); - String exprRegister = n.f3.accept(this, null); + String exprRegister = sizeExpression.accept(this, null); // labels String throwExLabel = getNewArrayAllocLabelName(); String allocateMemLabel = getNewArrayAllocLabelName(); // code String compareCode = "\t" + compareRegister + " = icmp slt i32 " + exprRegister + ", 0\n"; - String branchCode = "\tbr i1" + compareRegister + ", label %" + throwExLabel + ", label %" + allocateMemLabel + "\n"; + String branchCode = "\tbr i1 " + compareRegister + ", label %" + throwExLabel + ", label %" + allocateMemLabel + "\n"; String throwLabelCode = "\n" + throwExLabel + ":\n"; String callThrowCode = "\tcall void @throw_oob()\n"; String obligBranchCode = "\tbr label %" + allocateMemLabel + "\n"; @@ -602,6 +625,12 @@ public String visit(ArrayLookup n, Void argu) throws Exception { + throwOobCode + continueCode + continueLabelCode); + if ("boolean[]".equals(Main.ST.getArrayLookupType(n))){ + // boolean elements are stored as i32 + String boolRegister = getNewRegisterName(); + writeCode("\t" + boolRegister + " = trunc i32 " + returnRegister + " to i1\n"); + return boolRegister; + } return returnRegister; } diff --git a/Main.java b/Main.java index 7440698..05e23a4 100644 --- a/Main.java +++ b/Main.java @@ -2,51 +2,39 @@ import visitor.*; import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; public class Main { public static SymbolTable ST; - public static void main(String[] args) throws Exception { - + + public static void main(String[] args) { if (args.length == 0) { - System.err.println("Usage: java Main filename1 filename2 ...."); + System.err.println("Usage: java Main file1.java [file2.java ...]"); System.exit(1); } - FileInputStream fis = null; + boolean failed = false; for (String fileName : args) { - try{ - ST = new SymbolTable(); - fis = new FileInputStream(fileName); - MiniJavaParser parser = new MiniJavaParser(fis); - MyVisitor eval = new MyVisitor(); - CheckingVisitor checkingVisitor = new CheckingVisitor(); - LLVMVisitor llvm = new LLVMVisitor(); - llvm.LLVMFile(fileName); - Goal root = parser.Goal(); - try{ - try { - root.accept(eval, null); - root.accept(checkingVisitor, null); - root.accept(llvm, null); - } catch (Exception ex) { - System.err.println(ex.getMessage()); - } - } catch (Exception ex){ - System.out.println(ex.getMessage()); - } - }catch (ParseException ex) {// end big try - System.out.println(ex.getMessage()); - } catch (FileNotFoundException ex) { - System.err.println(ex.getMessage()); - } finally { - try { - if (fis != null) fis.close(); - } catch (IOException ex) { - System.err.println(ex.getMessage()); - } + try { + compile(fileName); + } catch (Exception ex) { + System.err.println(fileName + ": " + ex.getMessage()); + failed = true; } - } - + } + System.exit(failed ? 1 : 0); + } + + // Parses and type-checks the file, then writes the LLVM IR next to it (file.java -> file.ll) + private static void compile(String fileName) throws Exception { + ST = new SymbolTable(); + Goal root; + try (FileInputStream fis = new FileInputStream(fileName)) { + root = new MiniJavaParser(fis).Goal(); + } + root.accept(new MyVisitor(), null); + root.accept(new CheckingVisitor(), null); + + LLVMVisitor llvm = new LLVMVisitor(); + llvm.LLVMFile(fileName); + root.accept(llvm, null); } -} \ No newline at end of file +} diff --git a/MiniJavaClass.java b/MiniJavaClass.java index 8dc0d54..610dbde 100644 --- a/MiniJavaClass.java +++ b/MiniJavaClass.java @@ -59,19 +59,24 @@ public String toString() { public void insertClassMethod(MiniJavaMethod method){ String methodName = method.getMethodName(); - if (!this.methods.containsKey(methodName)){ + // methodNames is the vtable layout; the static main method is not part of it + if (!this.methods.containsKey(methodName) && !method.getReturnType().equals("void")){ this.methodNames.add(methodName); } this.methods.put(methodName, method); } + public boolean hasMethodVariable(String methodName, String idName){ + return this.methods.get(methodName).lookupVariable(idName) != null; + } + public void insertMethodVariable(String methodName, String idName, String idType){ this.methods.get(methodName).insertVariable(idName, idType); } public String lookupVariable(String idName, String currentMethodName){ String type = null; - if (currentMethodName !=""){ // if we are inside a function look for the identifier in the method variables + if (!currentMethodName.isEmpty()){ // if we are inside a function look for the identifier in the method variables MiniJavaMethod method = this.methods.get(currentMethodName); type = method.lookupVariable(idName); } @@ -110,6 +115,6 @@ public String getMethodOwner(String methodName){ } public int getNumberOfMethods(){ - return this.methods.size(); + return this.methodNames.size(); } } diff --git a/MyVisitor.java b/MyVisitor.java index 09b4785..3ec5983 100644 --- a/MyVisitor.java +++ b/MyVisitor.java @@ -36,8 +36,10 @@ public String visit(MainClass n, Void argu) throws Exception { Main.ST.enterMethod("main"); Main.ST.insertMethod("void"); Main.ST.insertMethodArgs(""); + // registered so that redeclaring it is caught; it cannot be used in expressions + Main.ST.insertPrimitive(n.f11.accept(this, null), SymbolTable.MAIN_ARGS_TYPE); - super.visit(n, argu); + n.f14.accept(this, null); Main.ST.exitMethod(); return null; } @@ -103,7 +105,8 @@ public String visit(MethodDeclaration n, Void argu) throws Exception { Main.ST.insertMethod(myType); String argumentList = n.f4.present() ? n.f4.accept(this, null) : ""; Main.ST.insertMethodArgs(argumentList); - super.visit(n, argu); + // only the local variables: visiting all children would insert the parameters twice + n.f7.accept(this, null); Main.ST.exitMethod(); return null; } diff --git a/README.md b/README.md index fe7f2e1..82dd245 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,99 @@ # Mini-Java-Compiler -I created a compiler for the MiniJava language, a simplified version of Java, as part of my coursework in the Compilers course at the Department of Informatics and Telecommunications. +[![tests](https://github.com/angelosSfyr/Mini-Java-Compiler/actions/workflows/tests.yml/badge.svg)](https://github.com/angelosSfyr/Mini-Java-Compiler/actions/workflows/tests.yml) -# The Compilation Process -The compiler converts MiniJava code into LLVM's intermediate representation. It operates through the following steps: +A compiler for [MiniJava](https://www.cambridge.org/resources/052182060X/), a subset of Java, that produces +[LLVM IR](https://llvm.org/docs/LangRef.html). The generated IR is compiled to a native executable with clang. +Built for the Compilers course at the Department of Informatics and Telecommunications, University of Athens. -Parsing MiniJava source files to create a parse tree and identify/report syntax errors. +## How it works -Traversing the parse tree using the Visitor pattern, building a symbol table and identifying semantic errors. +1. **Parsing** – a JavaCC parser, with the syntax tree and visitor interfaces generated by JTB, parses the source + file and reports syntax errors. +2. **Symbol table** – a first visitor (`MyVisitor`) collects classes, fields, methods and variables, + computes field and vtable offsets, and rejects duplicate declarations. +3. **Type checking** – a second visitor (`CheckingVisitor`) checks every statement and expression: + type compatibility with inheritance, method overriding rules, argument types, return types and more. +4. **Code generation** – a third visitor (`LLVMVisitor`) emits LLVM IR: objects with vtables for dynamic + dispatch, heap-allocated arrays with runtime bounds checks, and short-circuit `&&`. -Generating LLVM assembly language code using the Visitor pattern. +The compiler exits with a non-zero status and a descriptive message on the first error. -# Semantic Analysis and Type Checking -The compiler uses a symbol table to keep track of information about entities in the source code. Afterwards, the compiler performs type checking. If a MiniJava rule is violated, the compilation terminates and an error message is displayed. +## Example -# Intermediate Code Generation -The Visitor pattern and a selected set of LLVM instructions are utilized to produce LLVM's intermediate representation from the original MiniJava code. +```java +class Factorial { + public static void main(String[] a) { + System.out.println(new Fac().compute(5)); + } +} -# Technologies Used -Java +class Fac { + public int compute(int n) { + int result; + if (n < 1) + result = 1; + else + result = n * (this.compute(n - 1)); + return result; + } +} +``` -JavaCC +The recursive call becomes a virtual call through the object's vtable: -JTB +```llvm +define i32 @Fac.compute(i8* %this, i32 %.n) { + ... +if1: + %_11 = load i32, i32* %n + %_12 = bitcast i8* %this to i8*** + %_13 = load i8**, i8*** %_12 + %_14 = getelementptr i8*, i8** %_13, i32 0 + %_15 = load i8*, i8** %_14 + %_16 = bitcast i8* %_15 to i32(i8*, i32)* + %_18 = load i32, i32* %n + %_19 = sub i32 %_18, 1 + %_17 = call i32 %_16(i8* %this, i32 %_19) + %_20 = mul i32 %_11, %_17 + ... +} +``` -JFlex +A complete example is in [`Example.java`](Example.java) and its output in [`Example.ll`](Example.ll). -Java CUP +## Running it + +The Docker image contains everything needed (JDK, JavaCC/JTB, clang): + +```sh +docker build -t minijava . +docker run --rm minijava # run the test suite + +# compile and run your own program +docker run --rm -v "$PWD:/src" minijava bash -c \ + "java -cp /app Main /src/Program.java && clang -o /tmp/prog /src/Program.ll && /tmp/prog" +``` + +Without Docker you need a JDK and clang 14 or older (the generated IR uses typed pointers), then: + +```sh +make compile +java Main Program.java # writes Program.ll +clang -o program Program.ll && ./program +``` + +## Tests + +`run_tests.sh` runs the 87 programs in [`test/typechecking`](test/typechecking): + +- **`ERROR_*` programs** must be rejected by the compiler. +- **Valid programs** are compiled to LLVM IR and then to a native binary. Since MiniJava is a subset of Java, each + one is also compiled with `javac` and run on the JVM, and the two outputs must match, including programs that + end with an out-of-bounds array access. + +The suite runs on every push via GitHub Actions. + +## Technologies + +Java, JavaCC, JTB, LLVM IR, clang, Docker, GitHub Actions diff --git a/SymbolTable.java b/SymbolTable.java index f334e06..328d182 100644 --- a/SymbolTable.java +++ b/SymbolTable.java @@ -3,6 +3,8 @@ public class SymbolTable { + public static final String MAIN_ARGS_TYPE = "String[]"; + // offset stuff private int fieldOffset; private int methodOffset; @@ -16,6 +18,18 @@ public class SymbolTable { private String currentClass; private String currentMethod; + // array type ("int[]" / "boolean[]") of each ArrayLookup node, recorded during type checking + // so code generation knows when to convert stored i32 elements back to i1 + private IdentityHashMap arrayLookupTypes = new IdentityHashMap<>(); + + public void setArrayLookupType(Object lookupNode, String arrayType){ + this.arrayLookupTypes.put(lookupNode, arrayType); + } + + public String getArrayLookupType(Object lookupNode){ + return this.arrayLookupTypes.get(lookupNode); + } + SymbolTable() { @@ -43,8 +57,15 @@ public void exitMethod(){ this.currentMethod = ""; } - public void insertClass() - { + private void checkClassNotDeclared() throws Exception { + if (this.classes.containsKey(this.currentClass)) { + throw new Exception("Duplicate class: " + this.currentClass); + } + } + + public void insertClass() throws Exception + { + checkClassNotDeclared(); this.fieldOffset = 0; this.methodOffset = 0; this.lastClassFieldOffset.put(this.currentClass, 0); @@ -55,9 +76,10 @@ public void insertClass() } public void insertExtendedClass(String parentName) throws Exception{ + checkClassNotDeclared(); MiniJavaClass parent = this.classes.get(parentName); if (parent == null){ - throw new Exception("ClassExtendsDeclaration: Class"+parentName+"has not been declared yet"+"("+Main.ST.getCurrentMethodName()+")"); + throw new Exception("ClassExtendsDeclaration: Class "+parentName+" has not been declared yet"+"("+Main.ST.getCurrentMethodName()+")"); } this.fieldOffset = this.lastClassFieldOffset.get(parentName); @@ -74,19 +96,26 @@ public void insertMethod(String type)throws Exception{ boolean parentHasMethod=false; MiniJavaMethod method = new MiniJavaMethod(this.currentClass, this.currentMethod, type); MiniJavaClass methodClass = this.classes.get(this.currentClass); + // inherited methods may be overridden, but not redeclared in the same class + if (methodClass.hasMethod(this.currentMethod) + && methodClass.getMethodOwner(this.currentMethod).equals(this.currentClass)){ + throw new Exception("Duplicate method: " + this.currentMethod + " in class " + this.currentClass); + } // check if the parent has method with the same name [RETURN TYPE CHECK] String parentclassName = methodClass.getParentClassName(); - if (parentclassName != ""){ + if (!parentclassName.isEmpty()){ MiniJavaClass parentClass = this.classes.get(parentclassName); if (parentClass.hasMethod(this.currentMethod)){ parentHasMethod=true; String parentReturnType = parentClass.getMethodReturnType(this.currentMethod); - if (parentReturnType != type){ + if (!parentReturnType.equals(type)){ throw new Exception("MethodDeclaration: Different return type with parent "+"("+Main.ST.getCurrentMethodName()+")"); } } } - if (!parentHasMethod){ + // main (the only void method in MiniJava) is static, so it gets no vtable slot + boolean isMain = type.equals("void"); + if (!parentHasMethod && !isMain){ this.offsets.put(this.currentClass+"."+this.currentMethod+"()", this.methodOffset); this.methodOffset += 8; this.lastClassMethodOffset.put(this.currentClass, this.methodOffset); @@ -98,7 +127,7 @@ public void insertMethodArgs(String args) throws Exception { MiniJavaClass methodClass = this.classes.get(this.currentClass); // check if the parent has method with the same name [ARGS CHECK] String parentclassName = methodClass.getParentClassName(); - if (parentclassName != ""){ + if (!parentclassName.isEmpty()){ MiniJavaClass parentClass = this.classes.get(parentclassName); if (parentClass.hasMethod(this.currentMethod)){ String parentArgs= parentClass.getMethodArgs(this.currentMethod); @@ -111,24 +140,32 @@ public void insertMethodArgs(String args) throws Exception { } private int getOffestForType(String type){ - if (type=="int"){ + if (type.equals("int")){ return 4; - }else if(type == "boolean"){ + }else if(type.equals("boolean")){ return 1; } return 8; } - public void insertPrimitive(String idName, String idType) + public void insertPrimitive(String idName, String idType) throws Exception { MiniJavaClass c = this.classes.get(this.currentClass); - if (this.currentMethod == ""){ + if (this.currentMethod.isEmpty()){ + // offsets only holds fields declared in this class, so inherited fields may be shadowed + if (this.offsets.containsKey(this.currentClass + "." + idName)){ + throw new Exception("Duplicate field: " + idName + " in class " + this.currentClass); + } int offset = this.getOffestForType(idType); this.offsets.put(this.currentClass+"."+idName, this.fieldOffset); this.fieldOffset += offset; this.lastClassFieldOffset.put(this.currentClass, this.fieldOffset); c.insertField(idName, idType); }else{ + // parameters and local variables share one scope + if (c.hasMethodVariable(this.currentMethod, idName)){ + throw new Exception("Duplicate variable: " + idName + " in method " + this.currentMethod); + } c.insertMethodVariable(this.currentMethod, idName, idType); } } @@ -173,8 +210,10 @@ public boolean existsClass(String className){ } public boolean childHasParent(String childClassName, String parentclassName){ + if (childClassName == null) return false; MiniJavaClass child = this.classes.get(childClassName); - while(child.getParentClassName() != ""){ + if (child == null) return false; + while(!child.getParentClassName().isEmpty()){ if (child.getParentClassName().equals(parentclassName)) return true; child = this.classes.get(child.getParentClassName()); } @@ -183,7 +222,7 @@ public boolean childHasParent(String childClassName, String parentclassName){ public boolean classHadMethod(String className, String methodName) throws Exception { MiniJavaClass methodClass = this.classes.get(className); - if (className == null){ + if (methodClass == null){ throw new Exception("classObjectError: class does not exist"+"("+Main.ST.getCurrentMethodName()+")"); } return methodClass.hasMethod(methodName); @@ -236,9 +275,6 @@ public String getVtablesCode(){ while(itr.hasNext()){ currClass = itr.next(); MiniJavaClass cl = currClass.getValue(); - if (cl.hasMethod("main")){ - continue; - } String className = currClass.getKey(); int numberOfMethods = cl.getNumberOfMethods(); code += "@." + className + "_vtable = global [" + numberOfMethods + " x i8*] [\n"; diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..8990101 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Runs every program in test/typechecking: +# ERROR_*.java -> the compiler must reject it +# other *.java -> the compiler must accept it, and the program built from the generated +# LLVM IR must behave like the same program run with javac/java +# (MiniJava is a subset of Java, so real Java is the reference) +set -u +cd "$(dirname "$0")" + +# Valid Java, but not valid MiniJava: the argument of main cannot be used +MINIJAVA_ONLY_ERRORS=(length main_method_args_usage) + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +pass=0 +fail=0 +failures=() + +record_fail() { fail=$((fail + 1)); failures+=("$1"); } + +for src in test/typechecking/*.java; do + name=$(basename "$src" .java) + dir="$work/$name" + mkdir -p "$dir" + cp "$src" "$dir/prog.java" + + compile_output=$(java -cp . Main "$dir/prog.java" 2>&1) + compile_status=$? + + if [[ $name == ERROR_* || " ${MINIJAVA_ONLY_ERRORS[*]} " == *" $name "* ]]; then + if [[ $compile_status -ne 0 ]]; then + pass=$((pass + 1)) + else + record_fail "$name: expected a compile error, got none" + fi + continue + fi + + if [[ $compile_status -ne 0 ]]; then + record_fail "$name: unexpected compile error: ${compile_output#*: }" + continue + fi + + if ! clang -Wno-override-module -o "$dir/prog" "$dir/prog.ll" 2>"$dir/clang.err"; then + record_fail "$name: clang rejected the generated LLVM IR: $(head -1 "$dir/clang.err")" + continue + fi + actual=$(timeout 10 "$dir/prog" 2>/dev/null) + actual_status=$? + + main_class=$(grep -oP '^\s*class\s+\K\w+' "$dir/prog.java" | head -1) + mkdir -p "$dir/jvm" + if ! javac -nowarn -d "$dir/jvm" "$dir/prog.java" 2>/dev/null; then + # javac is stricter than MiniJava (e.g. it rejects reading unassigned locals, which MiniJava + # zero-initializes), so there is no Java reference: the program only has to run cleanly + if [[ $actual_status -eq 0 ]]; then + pass=$((pass + 1)) + else + record_fail "$name: program failed at runtime (no Java reference available)" + fi + continue + fi + expected=$(timeout 10 java -cp "$dir/jvm" "$main_class" 2>/dev/null) + expected_status=$? + + if [[ $expected_status -ne 0 ]]; then + # Java threw (e.g. ArrayIndexOutOfBoundsException): the compiled program must also fail, + # after printing the same output; it reports the error with its own final message + before_error="" + [[ $actual == *$'\n'* ]] && before_error=${actual%$'\n'*} + if [[ $actual_status -ne 0 && $before_error == "$expected" ]]; then + pass=$((pass + 1)) + else + record_fail "$name: expected a runtime error after the same output as Java" + fi + elif [[ $actual_status -eq 0 && $actual == "$expected" ]]; then + pass=$((pass + 1)) + else + record_fail "$name: output differs from Java" + fi +done + +for f in "${failures[@]}"; do echo "FAIL $f"; done +echo +echo "Passed: $pass Failed: $fail Total: $((pass + fail))" +[[ $fail -eq 0 ]] diff --git a/test/typechecking/Add (copy).java b/test/typechecking/Add (copy).java deleted file mode 100644 index a2245aa..0000000 --- a/test/typechecking/Add (copy).java +++ /dev/null @@ -1,5 +0,0 @@ -class Add { - public static void main(String[] a){ - System.out.println(12 + 21); - } -} diff --git a/test/typechecking/Add (copy).out b/test/typechecking/Add (copy).out deleted file mode 100644 index e69de29..0000000 diff --git a/test/typechecking/ArrayTest (copy).java b/test/typechecking/ArrayTest (copy).java deleted file mode 100644 index e49694e..0000000 --- a/test/typechecking/ArrayTest (copy).java +++ /dev/null @@ -1,25 +0,0 @@ -class ArrayTest{ - public static void main(String[] a){ - boolean n; - n = new Test().start(10); - } -} - -class Test { - - public boolean start(int sz){ - int[] b; - int l; - int i; - b = new int[sz]; - l = b.length; - i = 0; - while(i < (l)){ - b[i] = i; - System.out.println(b[i]); - i = i + 1; - } - return true; - } - -} diff --git a/test/typechecking/ArrayTest (copy).out b/test/typechecking/ArrayTest (copy).out deleted file mode 100644 index 030edc8..0000000 --- a/test/typechecking/ArrayTest (copy).out +++ /dev/null @@ -1 +0,0 @@ -Test.start: 0