From 444c252a1f345f392a9cb8238597ea7bcb96e6fa Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sat, 4 Jul 2026 22:55:19 +0100 Subject: Handle parenthesized sub expressions in Java AST --- .../output/lang/java/JavaAssociativity.java | 6 ++ .../compiler/output/lang/java/JavaPrecedence.java | 29 +++++++ .../compiler/output/lang/java/JavaWriter.java | 88 ++++++++++++++++++--- .../compiler/output/lang/java/JavaWriterTests.java | 90 ++++++++++++++++++---- 4 files changed, 189 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaAssociativity.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaPrecedence.java diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaAssociativity.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaAssociativity.java new file mode 100644 index 0000000..938c062 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaAssociativity.java @@ -0,0 +1,6 @@ +package org.zwobble.hobgoblin.compiler.output.lang.java; + +public enum JavaAssociativity { + LEFT, + RIGHT +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaPrecedence.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaPrecedence.java new file mode 100644 index 0000000..5318697 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaPrecedence.java @@ -0,0 +1,29 @@ +package org.zwobble.hobgoblin.compiler.output.lang.java; + +public enum JavaPrecedence { + PRIMARY(14), + POSTFIX(13), + UNARY(12), + MULTIPLICATIVE(11), + ADDITIVE(10), + SHIFT(9), + RELATIONAL(8), + EQUALITY(7), + BITWISE_AND(6), + BITWISE_EXCLUSIVE_OR(5), + BITWISE_INCLUSIVE_OR(4), + LOGICAL_AND(3), + LOGICAL_OR(2), + TERNARY(1), + ASSIGNMENT(0); + + private final int value; + + JavaPrecedence(int value) { + this.value = value; + } + + public int value() { + return this.value; + } +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java index e8e0f40..95e3081 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java @@ -40,7 +40,14 @@ public class JavaWriter implements AutoCloseable { } private void writeBinaryOperation(JavaBinaryOperation binaryOperation) throws IOException { - this.writeExpression(binaryOperation.left()); + var precedence = precedence(binaryOperation); + var isLeftAssociative = isLeftAssociative(binaryOperation.operator()); + + this.writeSubExpression( + binaryOperation.left(), + precedence, + isLeftAssociative + ); this.writer.write(" "); this.writer.write(switch (binaryOperation.operator()) { case ASSIGN -> "="; @@ -48,7 +55,19 @@ public class JavaWriter implements AutoCloseable { case UNSIGNED_RIGHT_SHIFT -> ">>>"; }); this.writer.write(" "); - this.writeExpression(binaryOperation.right()); + this.writeSubExpression( + binaryOperation.right(), + precedence, + !isLeftAssociative + ); + } + + private boolean isLeftAssociative(JavaBinaryOperator operator) { + return switch (operator) { + case ASSIGN -> false; + case BITWISE_AND -> true; + case UNSIGNED_RIGHT_SHIFT -> true; + }; } private void writeBooleanLiteral(JavaBooleanLiteral booleanLiteral) throws IOException { @@ -230,7 +249,7 @@ public class JavaWriter implements AutoCloseable { this.writer.write("}"); } - void writeExpression(JavaExpression expression) throws IOException { + void writeTopLevelExpression(JavaExpression expression) throws IOException { switch (expression) { case JavaBinaryOperation binaryOperation -> { writeBinaryOperation(binaryOperation); @@ -282,13 +301,60 @@ public class JavaWriter implements AutoCloseable { } } + void writeSubExpression( + JavaExpression expression, + JavaPrecedence parentPrecedence, + boolean matchesAssociativity + ) throws IOException { + var precedence = precedence(expression); + var requiresParens = parentPrecedence != JavaPrecedence.PRIMARY && ( + parentPrecedence.value() > precedence.value() || + (parentPrecedence.value() == precedence.value() && !matchesAssociativity) + ); + + if (requiresParens) { + this.writer.write("("); + } + + this.writeTopLevelExpression(expression); + + if (requiresParens) { + this.writer.write(")"); + } + } + + private JavaPrecedence precedence(JavaExpression expression) { + return switch (expression) { + case JavaBinaryOperation binaryOperation -> precedence(binaryOperation); + case JavaBooleanLiteral booleanLiteral -> JavaPrecedence.PRIMARY; + case JavaFieldAccess fieldAccess -> JavaPrecedence.PRIMARY; + case JavaIntegerLiteral integerLiteral -> JavaPrecedence.PRIMARY; + case JavaMethodCall methodCall -> JavaPrecedence.PRIMARY; + case JavaMethodRef methodRef -> JavaPrecedence.PRIMARY; + case JavaNewExpression newExpression -> JavaPrecedence.PRIMARY; + case JavaNullLiteral nullLiteral -> JavaPrecedence.PRIMARY; + case JavaRef ref -> JavaPrecedence.PRIMARY; + case JavaStaticFieldAccess staticFieldAccess -> JavaPrecedence.PRIMARY; + case JavaStaticMethodCall staticMethodCall -> JavaPrecedence.PRIMARY; + case JavaStringLiteral stringLiteral -> JavaPrecedence.PRIMARY; + }; + } + + private static JavaPrecedence precedence(JavaBinaryOperation binaryOperation) { + return switch (binaryOperation.operator()) { + case ASSIGN -> JavaPrecedence.ASSIGNMENT; + case BITWISE_AND -> JavaPrecedence.BITWISE_AND; + case UNSIGNED_RIGHT_SHIFT -> JavaPrecedence.SHIFT; + }; + } + private void writeExpressionStatement(JavaExpressionStatement expressionStatement) throws IOException { - writeExpression(expressionStatement.expression()); + writeTopLevelExpression(expressionStatement.expression()); this.writer.write(";"); } private void writeFieldAccess(JavaFieldAccess fieldAccess) throws IOException { - this.writeExpression(fieldAccess.receiver()); + this.writeSubExpression(fieldAccess.receiver(), JavaPrecedence.PRIMARY, true); this.writer.write("."); this.writeIdentifier(fieldAccess.fieldName()); } @@ -370,18 +436,18 @@ public class JavaWriter implements AutoCloseable { this.writer.write("var "); this.writeIdentifier(localVariableDeclaration.name()); this.writer.write(" = "); - this.writeExpression(localVariableDeclaration.initializer()); + this.writeTopLevelExpression(localVariableDeclaration.initializer()); this.writer.write(";"); } private void writeMethodCall(JavaMethodCall call) throws IOException { - this.writeExpression(call.receiver()); + this.writeSubExpression(call.receiver(), JavaPrecedence.PRIMARY, true); this.writer.write("."); this.writeIdentifier(call.methodName()); this.writer.write("("); writeWithSeparator( call.args(), - this::writeExpression, + this::writeTopLevelExpression, () -> this.writer.write(", ") ); this.writer.write(")"); @@ -443,7 +509,7 @@ public class JavaWriter implements AutoCloseable { this.writer.write("("); writeWithSeparator( newExpression.args(), - this::writeExpression, + this::writeTopLevelExpression, () -> this.writer.write(", ") ); this.writer.write(")"); @@ -504,7 +570,7 @@ public class JavaWriter implements AutoCloseable { private void writeReturnStatement(JavaReturn returnStatement) throws IOException { this.writer.write("return "); - writeExpression(returnStatement.value()); + writeTopLevelExpression(returnStatement.value()); this.writer.write(";"); } @@ -521,7 +587,7 @@ public class JavaWriter implements AutoCloseable { this.writer.write("("); writeWithSeparator( call.args(), - this::writeExpression, + this::writeTopLevelExpression, () -> this.writer.write(", ") ); this.writer.write(")"); diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java index e1e43ad..25b2c9f 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java @@ -794,7 +794,7 @@ public class JavaWriterTests { public void falseLiteral() throws IOException { var java = new JavaBooleanLiteral(false); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("false")); } @@ -803,7 +803,7 @@ public class JavaWriterTests { public void trueLiteral() throws IOException { var java = new JavaBooleanLiteral(true); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("true")); } @@ -812,7 +812,7 @@ public class JavaWriterTests { public void integerLiteral() throws IOException { var java = new JavaIntegerLiteral(42); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("42")); } @@ -821,7 +821,7 @@ public class JavaWriterTests { public void nullLiteral() throws IOException { var java = new JavaNullLiteral(); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("null")); } @@ -830,7 +830,7 @@ public class JavaWriterTests { public void stringLiteral() throws IOException { var java = new JavaStringLiteral("hello"); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("\"hello\"")); } @@ -843,7 +843,7 @@ public class JavaWriterTests { new JavaIntegerLiteral(42) ); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("x = 42")); } @@ -856,7 +856,7 @@ public class JavaWriterTests { .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("y"))) .build(); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("x >>> y")); } @@ -869,7 +869,7 @@ public class JavaWriterTests { .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("y"))) .build(); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("x & y")); } @@ -881,7 +881,7 @@ public class JavaWriterTests { JavaIdentifier.of("two") ); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("one.two")); } @@ -893,7 +893,7 @@ public class JavaWriterTests { JavaIdentifier.of("three") ); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("one.Two.three")); } @@ -905,7 +905,7 @@ public class JavaWriterTests { JavaIdentifier.of("three") ); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("one.Two::three")); } @@ -917,7 +917,7 @@ public class JavaWriterTests { List.of(new JavaIntegerLiteral(1), new JavaIntegerLiteral(2)) ); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("new one.Two(1, 2)")); } @@ -926,11 +926,75 @@ public class JavaWriterTests { public void ref() throws IOException { var java = new JavaRef(JavaIdentifier.of("x")); - var string = write(writer -> writer.writeExpression(java)); + var string = write(writer -> writer.writeTopLevelExpression(java)); assertThat(string, equalTo("x")); } + @Test + public void whenSubExpressionHasLowerPrecedenceThenSubExpressionIsParenthesized() throws IOException { + var java = JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.BITWISE_AND) + .withLeft( + JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.ASSIGN) + .withLeft(JavaRef.arbitrary().withName(JavaIdentifier.of("x"))) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("y"))) + ) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("z"))) + .build(); + + var string = write(writer -> writer.writeTopLevelExpression(java)); + + assertThat(string, equalTo("(x = y) & z")); + } + + @Test + public void whenSubExpressionsOfLeftAssociativeBinaryOperationHaveSamePrecedenceThenRightExpressionIsParenthesized() throws IOException { + var java = JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.BITWISE_AND) + .withLeft( + JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.BITWISE_AND) + .withLeft(JavaRef.arbitrary().withName(JavaIdentifier.of("a"))) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("b"))) + ) + .withRight( + JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.BITWISE_AND) + .withLeft(JavaRef.arbitrary().withName(JavaIdentifier.of("c"))) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("d"))) + ) + .build(); + + var string = write(writer -> writer.writeTopLevelExpression(java)); + + assertThat(string, equalTo("a & b & (c & d)")); + } + + @Test + public void whenSubExpressionsOfRightAssociativeBinaryOperationHaveSamePrecedenceThenLeftExpressionIsParenthesized() throws IOException { + var java = JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.ASSIGN) + .withLeft( + JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.ASSIGN) + .withLeft(JavaRef.arbitrary().withName(JavaIdentifier.of("a"))) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("b"))) + ) + .withRight( + JavaBinaryOperation.arbitrary() + .withOperator(JavaBinaryOperator.ASSIGN) + .withLeft(JavaRef.arbitrary().withName(JavaIdentifier.of("c"))) + .withRight(JavaRef.arbitrary().withName(JavaIdentifier.of("d"))) + ) + .build(); + + var string = write(writer -> writer.writeTopLevelExpression(java)); + + assertThat(string, equalTo("(a = b) = c = d")); + } + @Test public void typeRefWithoutArgsHasNoAngleBrackets() throws IOException { var java = JavaTypeRef.topLevel( -- cgit v1.2.3