Skip to content

Commit

Permalink
feat: deep and shallow equals
Browse files Browse the repository at this point in the history
  • Loading branch information
Eddie authored and Eddie committed Jun 19, 2024
1 parent 493db11 commit 2cb5939
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 29 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A toy programming language built for the Java Virtual Machine.

- Read from standard input
- Accept command-line arguments
- String parsing
- String parsing and manipulation
- Invoke static method in class file
- Data types: Struct, Set, Map, List, Array

Expand Down
Binary file modified out/artifacts/jungle_jar/jungle.jar
Binary file not shown.
18 changes: 18 additions & 0 deletions programs/strings.source
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# shallow comparison `int String::hashCode()`

assert (equals "" 0)
assert (equals "A" 65)
assert (equals "shallow" 2054046228)
assert (equals 2054046228 "shallow")

# deep comparison using `boolean String::equals(Object)`

assert (equals
"abc123"
"abc123"
)

assert (not equals
"0-42L"
"0-43-"
)
3 changes: 3 additions & 0 deletions src/main/java/com/jungle/ast/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import java.util.Stack;

public class Node implements INode {
@NotNull
public static final Node NOOP = new Node(NodeType.NOOP);

@NotNull
private final NodeType type;

Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/jungle/ast/NodeType.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public enum NodeType {
CAST_FLOAT,
CAST_DOUBLE,

// No operation
NOOP,

// region Binary Operators - math
OPERATOR_ADD,
OPERATOR_SUBTRACT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
import com.jungle.ast.INode;
import com.jungle.ast.Node;
import com.jungle.ast.NodeType;
import com.jungle.common.SetUtils;
import com.jungle.compiler.ICompilerOptions;
import com.jungle.compiler.operand.OperandStackContext;
import com.jungle.compiler.operand.OperandType;
import com.jungle.logger.FileLogger;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class BooleanOperatorVisitor extends AbstractVisitor {
Expand All @@ -26,7 +27,7 @@ public class BooleanOperatorVisitor extends AbstractVisitor {
private static final INode PUSH_FALSE_NODE = new Node(NodeType.LITERAL_BOOLEAN).withRawValue("false");

@NotNull
private static final Set<NodeType> BOOLEAN_OPERATORS = new HashSet<>(Arrays.asList(
private static final Set<NodeType> BOOLEAN_OPERATORS = SetUtils.newSet(
// unary
NodeType.OPERATOR_NOT,
// binary
Expand All @@ -36,7 +37,7 @@ public class BooleanOperatorVisitor extends AbstractVisitor {
NodeType.OPERATOR_EQUAL,
NodeType.OPERATOR_LESS_THAN,
NodeType.OPERATOR_GREATER_THAN
));
);

@Nullable
private IfVisitor ifVisitor;
Expand All @@ -49,6 +50,17 @@ private IfVisitor getIfVisitor() {
return ifVisitor;
}

@Nullable
private ExpressionVisitor expressionVisitor;

@NotNull
private ExpressionVisitor getExpressionVisitor() {
if (expressionVisitor == null) {
expressionVisitor = new ExpressionVisitor(getCompilerOptions());
}
return expressionVisitor;
}

public BooleanOperatorVisitor(@NotNull ICompilerOptions options) {
super(options);
}
Expand Down Expand Up @@ -131,19 +143,56 @@ public void visit(
* if (result == 0) return true
* else return false
*/

/*
* An equals boolean operator should be able to handle objects and primitives.
* The operator should also be able to handle shallow and deep comparisons of types.
*
* When the equals operator is visited, then we need to determine the expected operand type.
* However, this requires use to visit the left and right expressions.
* If the types are both objects, then we can compare the objects using `boolean Object::equals(Object)`.
* Otherwise, we need to evaluate a new expression but the challenge is that the left and right AST has already been evaluated.
*
* Solution: For now, we push the types back onto the stack and introduce a no-op which allows us to assume the values are already on the operand stack.
*
* TODO: Should the IfVisitor be re-written to assume the expression is already compiled?
*/

if (ast.getRight() == null) {
throw new Error("boolean operator missing right expression");
}
getIfVisitor().visit(

getExpressionVisitor().visit(mv, ast.getLeft(), context);
OperandType leftType = context.pop();

getExpressionVisitor().visit(mv, ast.getRight(), context);
OperandType rightType = context.pop();

if (leftType == OperandType.OBJECT && rightType == OperandType.OBJECT) {
// Deep comparison
// invoke int Object::equals()
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Object",
"equals",
"(Ljava/lang/Object;)Z",
false
);
context.push(OperandType.INTEGER); // final type
} else {
Node conditionNode = new Node(NodeType.OPERATOR_SUBTRACT).withLeft(Node.NOOP).withRight(Node.NOOP);
context.push(rightType);
context.push(leftType);
getExpressionVisitor().visit(mv, conditionNode, context);
getIfVisitor().visit(
mv,
CompareTo.NONZERO, // when non-0 (true), jump to else
new Node(NodeType.OPERATOR_SUBTRACT)
.withLeft(ast.getLeft())
.withRight(ast.getRight()),
Node.NOOP, // use what is currently on the stack
PUSH_TRUE_NODE,
PUSH_FALSE_NODE,
context
);
);
}
} break;
case OPERATOR_LESS_THAN: {
/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jungle.compiler.visitor;

import com.jungle.ast.INode;
import com.jungle.ast.NodeType;
import com.jungle.compiler.ICompilerOptions;
import com.jungle.compiler.operand.OperandStackContext;
import com.jungle.logger.FileLogger;
Expand Down Expand Up @@ -88,6 +89,11 @@ public void visit(
throw new Error("expected expression");
}

if (ast.getType() == NodeType.NOOP) {
logger.debug("no-op");
return;
}

if (getIdentifierVisitor().canVisit(ast)) {
getIdentifierVisitor().visit(mv, ast, context);
return;
Expand Down
22 changes: 8 additions & 14 deletions src/main/java/com/jungle/compiler/visitor/IfVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,15 @@ protected void visit(
}

OperandType conditionType = null;
try {
getExpressionVisitor().visit(mv, conditionNode, context);
} catch (Throwable t) {
boolean hasEvaluatedCondition = !context.isEmpty();
if (hasEvaluatedCondition) {
conditionType = context.peek();
// Expression may not be an integer like primitive
switch (conditionType) {
case OBJECT: {
throw new Error("not implemented");
}
default: break;
}

if (conditionNode.getType() == NodeType.NOOP) {
logger.debug("assuming that the condition result is already on the operand stack");
} else {
try {
getExpressionVisitor().visit(mv, conditionNode, context);
} catch (Throwable t) {
throw new Error("if condition/expression cannot be evaluated", t);
}
throw new Error("if condition/expression cannot be evaluated", t);
}

conditionType = context.peek();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,23 @@ private OperandType convert(
@NotNull OperandType fromType,
@NotNull OperandType toType
) {
if (fromType == OperandType.INTEGER && toType == OperandType.CHAR) {
if (fromType == OperandType.INTEGER && toType == OperandType.CHAR) {
mv.visitInsn(Opcodes.I2C);
context.push(OperandType.CHAR); // final type
return OperandType.INTEGER; // computation type
}
if (fromType == OperandType.OBJECT && toType == OperandType.INTEGER) {
// invoke int Object::hashCode() on both left and right objects
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Object",
"hashCode",
"()I",
false
);
context.push(OperandType.INTEGER); // final type
return OperandType.INTEGER; // computation type
}
throw new Error("conversion not possible");
}

Expand Down Expand Up @@ -106,10 +118,7 @@ public void visit(
*
* We want explicit conversions, right?
*/
if (leftExpressionType.equals(rightExpressionType)) {
operandType = leftExpressionType;
context.push(operandType);
} else if (leftExpressionType == OperandType.INTEGER && rightExpressionType == OperandType.CHAR) {
if (leftExpressionType == OperandType.INTEGER && rightExpressionType == OperandType.CHAR) {
// Note: left ast is previous operand stack item
mv.visitInsn(Opcodes.SWAP);
operandType = convert(mv, ast, context, OperandType.INTEGER, OperandType.CHAR);
Expand All @@ -118,6 +127,18 @@ public void visit(
} else if (leftExpressionType == OperandType.CHAR && rightExpressionType == OperandType.INTEGER) {
// Note: right ast is next operand stack item
operandType = convert(mv, ast, context, OperandType.INTEGER, OperandType.CHAR);
} else if (leftExpressionType == OperandType.OBJECT) {
// Shallow comparison
mv.visitInsn(Opcodes.SWAP); // switch to object
operandType = convert(mv, ast, context, OperandType.OBJECT, OperandType.INTEGER);
mv.visitInsn(Opcodes.SWAP); // restore order
} else if (rightExpressionType == OperandType.OBJECT) {
// Shallow comparison
operandType = convert(mv, ast, context, OperandType.OBJECT, OperandType.INTEGER);
}
else if (leftExpressionType.equals(rightExpressionType)) {
operandType = leftExpressionType;
context.push(operandType);
} else {
throw new Error("binary operator left or right expression requires type cast " + ast);
}
Expand Down

0 comments on commit 2cb5939

Please sign in to comment.